From bf1bc28d81744c35ce740070fb921e5c86e7d9d2 Mon Sep 17 00:00:00 2001 From: William Horn Date: Wed, 19 Nov 2025 14:17:23 -0900 Subject: [PATCH 01/59] Add ability to update a jobs project name --- .../scene-file/scene-file.component.html | 22 ++++++++++++++++++- .../scene-file/scene-file.component.ts | 15 +++++++++++++ .../scene-file/scene-file.module.ts | 2 ++ .../scene-files/scene-files.component.html | 1 + .../scene-files/scene-files.component.ts | 8 +++++++ src/app/services/hyp3/hyp3-api.service.ts | 8 +++++++ src/app/services/hyp3/hyp3-job.service.ts | 2 +- src/app/store/scenes/scenes.action.ts | 9 ++++++++ src/app/store/scenes/scenes.reducer.ts | 20 +++++++++++++++++ 9 files changed, 85 insertions(+), 2 deletions(-) diff --git a/src/app/components/results-menu/scene-files/scene-file/scene-file.component.html b/src/app/components/results-menu/scene-files/scene-file/scene-file.component.html index 6d428b7ef..441ef26fc 100644 --- a/src/app/components/results-menu/scene-files/scene-file/scene-file.component.html +++ b/src/app/components/results-menu/scene-files/scene-file/scene-file.component.html @@ -137,7 +137,27 @@ @if (product.metadata.job) {
- {{ 'PROJECT_NAME' | translate }}: {{ product.metadata.job.name }} + {{ 'PROJECT_NAME' | translate }}: {{ product.metadata.job.name }} + + @if (!isEditingProjectName) { + + } + + @if (isEditingProjectName) { +
+ + +
+ } + +
diff --git a/src/app/components/results-menu/scene-files/scene-file/scene-file.component.ts b/src/app/components/results-menu/scene-files/scene-file/scene-file.component.ts index 44a43e612..5e7448e5d 100644 --- a/src/app/components/results-menu/scene-files/scene-file/scene-file.component.ts +++ b/src/app/components/results-menu/scene-files/scene-file/scene-file.component.ts @@ -53,6 +53,7 @@ export class SceneFileComponent implements OnInit, OnDestroy { @Output() unzip = new EventEmitter(); @Output() closeProduct = new EventEmitter(); @Output() queueHyp3Job = new EventEmitter(); + @Output() renameJobProjectName = new EventEmitter(); public searchType$ = this.store$.select(searchStore.getSearchType); public searchTypes = SearchType; @@ -60,6 +61,9 @@ export class SceneFileComponent implements OnInit, OnDestroy { public paramsList = []; public copyIcons = models.CopyIcons; + public newProjectName = ''; + public isEditingProjectName = false; + private subs = new SubSink(); ngOnInit() { @@ -155,6 +159,17 @@ export class SceneFileComponent implements OnInit, OnDestroy { ); } + public onEditProjectName(oldProjectName: string) { + this.newProjectName = oldProjectName; + this.isEditingProjectName = true; + } + + public onSubmitProjectName() { + this.renameJobProjectName.emit(this.newProjectName); + this.isEditingProjectName = false; + this.newProjectName = ''; + } + private expirationDays(expiration_time: moment.Moment): number { const current = moment.utc(); diff --git a/src/app/components/results-menu/scene-files/scene-file/scene-file.module.ts b/src/app/components/results-menu/scene-files/scene-file/scene-file.module.ts index 1b48cdb9b..006679a57 100644 --- a/src/app/components/results-menu/scene-files/scene-file/scene-file.module.ts +++ b/src/app/components/results-menu/scene-files/scene-file/scene-file.module.ts @@ -1,5 +1,6 @@ import { NgModule, inject } from '@angular/core'; import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; import { MatChipsModule } from '@angular/material/chips'; import { MatMenuModule } from '@angular/material/menu'; @@ -23,6 +24,7 @@ import { SharedModule } from '@shared'; declarations: [SceneFileComponent], imports: [ CommonModule, + FormsModule, FontAwesomeModule, MatSharedModule, MatMenuModule, diff --git a/src/app/components/results-menu/scene-files/scene-files.component.html b/src/app/components/results-menu/scene-files/scene-files.component.html index 31263457c..9f1555c42 100644 --- a/src/app/components/results-menu/scene-files/scene-files.component.html +++ b/src/app/components/results-menu/scene-files/scene-files.component.html @@ -16,6 +16,7 @@ (toggle)="onToggleQueueProduct(product)" (unzip)="onOpenUnzipProduct($event)" (queueHyp3Job)="onQueueHyp3Job($event)" + (renameJobProjectName)="onRenameJobProjectName(product, $event)" [loadingHyp3JobName]="loadingHyp3JobName" [isUserLoggedIn]="isUserLoggedIn" [product]="product" diff --git a/src/app/components/results-menu/scene-files/scene-files.component.ts b/src/app/components/results-menu/scene-files/scene-files.component.ts index 485c2ed39..1203a54a8 100644 --- a/src/app/components/results-menu/scene-files/scene-files.component.ts +++ b/src/app/components/results-menu/scene-files/scene-files.component.ts @@ -328,6 +328,14 @@ export class SceneFilesComponent this.store$.dispatch(new queueStore.AddJob(job)); } + public onRenameJobProjectName(product: models.CMRProduct, newProjectName: string) { + const job = product.metadata.job; + this.hyp3.updateJobName$(job.job_id, newProjectName).subscribe((job) => { + const action = new scenesStore.UpdateProductWithNewProjectName({ productId: product.id, name: job.name }); + this.store$.dispatch(action); + }); + } + public formatProductName(product_name: string, desiredLen?: number) { const extrasWidthPx = 260; const charWidthPx = 10; diff --git a/src/app/services/hyp3/hyp3-api.service.ts b/src/app/services/hyp3/hyp3-api.service.ts index 40df86b7e..cac33947f 100644 --- a/src/app/services/hyp3/hyp3-api.service.ts +++ b/src/app/services/hyp3/hyp3-api.service.ts @@ -157,6 +157,14 @@ export class Hyp3ApiService { ); } + public updateJobName$(jobId: string, projectName: string): Observable { + const url = `${this.apiUrl}/jobs/${jobId}`; + + return this.http.patch(url, { name: projectName }, { withCredentials: true }) + .pipe(map((resp) => resp as models.Hyp3Job) + ); + } + public submitJobBatch$(jobBatch: object) { const submitJobUrl = `${this.apiUrl}/jobs`; diff --git a/src/app/services/hyp3/hyp3-job.service.ts b/src/app/services/hyp3/hyp3-job.service.ts index 217cc2c5d..adf65d60a 100644 --- a/src/app/services/hyp3/hyp3-job.service.ts +++ b/src/app/services/hyp3/hyp3-job.service.ts @@ -195,7 +195,7 @@ export class Hyp3JobService { return newJobProducts; } - private combineJobAndCmrProduct( + public combineJobAndCmrProduct( job: models.Hyp3Job, product: models.CMRProduct, ) { diff --git a/src/app/store/scenes/scenes.action.ts b/src/app/store/scenes/scenes.action.ts index 3a9576994..13a224d8b 100644 --- a/src/app/store/scenes/scenes.action.ts +++ b/src/app/store/scenes/scenes.action.ts @@ -9,6 +9,7 @@ import { SarviewsEvent, SarviewsProduct, CMRProductsById, + // Hyp3Job, } from '@models'; import { PinnedProduct } from '@services/browse-map.service'; @@ -29,6 +30,7 @@ export enum ScenesActionType { CLOSE_ZIP_CONTENTS = '[Scenes] Close Zip Contents', ADD_CMR_DATA_TO_ON_DEMAND_JOBS = '[Scenes] Add CMR Data to On Demand Jobs', + UPDATE_PRODUCT_WITH_NEW_PROJECT_NAME = '[Scenes] Update proudct with new project name', SET_SELECTED_SCENE = '[Scenes] Set Selected Scene', SET_SELECTED_PAIR = '[Scenes] Set Selected Pair', @@ -190,6 +192,12 @@ export class AddCmrDataToOnDemandScenes implements Action { constructor(public payload: CMRProductsById) {} } +export class UpdateProductWithNewProjectName implements Action { + public readonly type = ScenesActionType.UPDATE_PRODUCT_WITH_NEW_PROJECT_NAME; + + constructor(public payload: { productId: string; name: string }) {} +} + export type ScenesActions = | SetScenes | ClearScenes @@ -214,4 +222,5 @@ export type ScenesActions = | SetSarviewsEventProducts | SetSelectedSarviewProduct | AddCmrDataToOnDemandScenes + | UpdateProductWithNewProjectName | SetImageBrowseProducts; diff --git a/src/app/store/scenes/scenes.reducer.ts b/src/app/store/scenes/scenes.reducer.ts index f748f132c..db4ae51b2 100644 --- a/src/app/store/scenes/scenes.reducer.ts +++ b/src/app/store/scenes/scenes.reducer.ts @@ -184,6 +184,26 @@ export function scenesReducer( } } + case ScenesActionType.UPDATE_PRODUCT_WITH_NEW_PROJECT_NAME: { + const { productId, name } = action.payload; + const products = { ...state.products }; + + const toUpdate = products[productId]; + + products[productId] = { + ...toUpdate, + metadata: { + ...toUpdate.metadata, + job: { + ...toUpdate.metadata.job, + name, + }, + }, + }; + + return { ...state, products }; + } + case ScenesActionType.SET_SELECTED_SCENE: { return { ...state, From df0e9fabe38a69ab8da666d0da6bff381e9e2efe Mon Sep 17 00:00:00 2001 From: William Horn Date: Wed, 19 Nov 2025 14:21:20 -0900 Subject: [PATCH 02/59] fix: formatting --- src/app/store/scenes/scenes.reducer.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/app/store/scenes/scenes.reducer.ts b/src/app/store/scenes/scenes.reducer.ts index db4ae51b2..4358fe9e1 100644 --- a/src/app/store/scenes/scenes.reducer.ts +++ b/src/app/store/scenes/scenes.reducer.ts @@ -193,11 +193,11 @@ export function scenesReducer( products[productId] = { ...toUpdate, metadata: { - ...toUpdate.metadata, - job: { - ...toUpdate.metadata.job, - name, - }, + ...toUpdate.metadata, + job: { + ...toUpdate.metadata.job, + name, + }, }, }; From 0a83c1a19d6c08244377e9350379287b8a4c984e Mon Sep 17 00:00:00 2001 From: William Horn Date: Wed, 19 Nov 2025 15:20:40 -0900 Subject: [PATCH 03/59] Add ability to nuke all loaded jobs --- .../scenes-list-header.component.html | 45 +++++++++++++++++++ .../scenes-list-header.component.ts | 33 +++++++++++++- .../scenes-list-header.module.ts | 2 + src/app/services/hyp3/hyp3-api.service.ts | 3 +- 4 files changed, 80 insertions(+), 3 deletions(-) diff --git a/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.html b/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.html index b3832c2b2..f4ed610d1 100644 --- a/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.html +++ b/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.html @@ -322,8 +322,53 @@
+ @if (!isEditingProjectName) { +
+
+ +
+
+
+ + + edit + + +
+
+
+ + +
+ +
+
+ } } + @if (isEditingProjectName) { +
+ + +
+ } + +
{ @@ -640,6 +645,32 @@ export class ScenesListHeaderComponent implements OnInit, OnDestroy { }); } + public onBulkProjectNameUpdate() { + this.isEditingProjectName = true; + this.newProjectName = ''; + } + + public onSubmitProjectName() { + this.isEditingProjectName = false; + const newName = this.newProjectName; + + from(this.products).pipe( + mergeMap( + (product) => { + console.log(product.metadata.job.job_id); + return this.hyp3.updateJobName$(product.metadata.job.job_id, newName); + }, + 20 + ), + toArray(), + ).subscribe(resps => { + console.log(resps); + }); + + console.log(`updating with ${this.newProjectName}`); + this.newProjectName = ''; + } + ngOnDestroy(): void { this.subs.unsubscribe(); } diff --git a/src/app/components/results-menu/scenes-list-header/scenes-list-header.module.ts b/src/app/components/results-menu/scenes-list-header/scenes-list-header.module.ts index 32275a50b..79abc33af 100644 --- a/src/app/components/results-menu/scenes-list-header/scenes-list-header.module.ts +++ b/src/app/components/results-menu/scenes-list-header/scenes-list-header.module.ts @@ -1,5 +1,6 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; import { MatMenuModule } from '@angular/material/menu'; import { MatButtonToggleModule } from '@angular/material/button-toggle'; @@ -15,6 +16,7 @@ import { MatIconModule } from '@angular/material/icon'; declarations: [ScenesListHeaderComponent], imports: [ CommonModule, + FormsModule, MatMenuModule, MatIconModule, MatSharedModule, diff --git a/src/app/services/hyp3/hyp3-api.service.ts b/src/app/services/hyp3/hyp3-api.service.ts index cac33947f..49e099159 100644 --- a/src/app/services/hyp3/hyp3-api.service.ts +++ b/src/app/services/hyp3/hyp3-api.service.ts @@ -161,8 +161,7 @@ export class Hyp3ApiService { const url = `${this.apiUrl}/jobs/${jobId}`; return this.http.patch(url, { name: projectName }, { withCredentials: true }) - .pipe(map((resp) => resp as models.Hyp3Job) - ); + .pipe(map((resp) => resp as models.Hyp3Job)); } public submitJobBatch$(jobBatch: object) { From b1cf75150667d7e28b210057378f9c467cbaf587 Mon Sep 17 00:00:00 2001 From: Andrew Anderson Date: Mon, 1 Dec 2025 13:33:09 -0800 Subject: [PATCH 04/59] Make Help more easily seen on Filter's panel. --- .../dataset-filters.component.html | 28 +++++++++++-------- .../dataset-filters.component.scss | 14 +++++++++- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/src/app/components/filters-dropdown/dataset-filters/dataset-filters.component.html b/src/app/components/filters-dropdown/dataset-filters/dataset-filters.component.html index ecf6033ca..73321a373 100644 --- a/src/app/components/filters-dropdown/dataset-filters/dataset-filters.component.html +++ b/src/app/components/filters-dropdown/dataset-filters/dataset-filters.component.html @@ -38,10 +38,11 @@ [collapsedHeight]="customCollapsedHeight" [expandedHeight]="customExpandedHeight" > - + {{ 'AREA_OF_INTEREST_OPTIONS' | translate }} @@ -63,10 +64,11 @@ [collapsedHeight]="customCollapsedHeight" [expandedHeight]="customExpandedHeight" > - + {{ 'DATE_FILTERS' | translate }} @@ -91,10 +93,11 @@ [collapsedHeight]="customCollapsedHeight" [expandedHeight]="customExpandedHeight" > - + {{ 'PRODUCT_FILTERS' | translate }} @@ -118,18 +121,20 @@ [collapsedHeight]="customCollapsedHeight" [expandedHeight]="customExpandedHeight" > - + @if (selectedDataset !== 'NISAR') { {{ 'ADDITIONAL_FILTERS' | translate }} } @else { {{ 'OBSERVATIONAL_FILTERS' | translate }} @@ -157,7 +162,7 @@ [collapsedHeight]="customCollapsedHeight" [expandedHeight]="customExpandedHeight" > - + @if (selectedDataset === 'SENTINEL-1 INTERFEROGRAM (BETA)') { {{ 'TRACK' | translate }} {{ 'FILTERS' | translate }} } @else if (selectedDataset !== 'NISAR') { @@ -166,7 +171,8 @@ {{ 'TRACK_AND_FRAME_FILTERS' | translate }} } diff --git a/src/app/components/filters-dropdown/dataset-filters/dataset-filters.component.scss b/src/app/components/filters-dropdown/dataset-filters/dataset-filters.component.scss index 42874ce74..ecbcdb19f 100644 --- a/src/app/components/filters-dropdown/dataset-filters/dataset-filters.component.scss +++ b/src/app/components/filters-dropdown/dataset-filters/dataset-filters.component.scss @@ -9,14 +9,26 @@ .selector-card-spacing { margin-top: 20px; } +.panel-title { + align-items: center; + justify-content: space-between; +} .info-icon { margin-left: 10px; margin-top: 4px; } + +.info-text { + flex: none; + font-size: small; + font-style: italic; + font-weight: 400; +} + .header { @include themify($themes) { background-color: themed('primary-light'); } -} \ No newline at end of file +} From 37774c9a698167e03311bcc8eabf8f9923d1a081 Mon Sep 17 00:00:00 2001 From: Andrew Anderson Date: Mon, 1 Dec 2025 14:23:26 -0800 Subject: [PATCH 05/59] DS-6336 Text modified to be translatable. --- .../dataset-filters/dataset-filters.component.html | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/app/components/filters-dropdown/dataset-filters/dataset-filters.component.html b/src/app/components/filters-dropdown/dataset-filters/dataset-filters.component.html index 73321a373..fdc73c3e5 100644 --- a/src/app/components/filters-dropdown/dataset-filters/dataset-filters.component.html +++ b/src/app/components/filters-dropdown/dataset-filters/dataset-filters.component.html @@ -42,7 +42,7 @@ {{ 'AREA_OF_INTEREST_OPTIONS' | translate }} @@ -68,7 +68,7 @@ {{ 'DATE_FILTERS' | translate }} @@ -97,7 +97,7 @@ {{ 'PRODUCT_FILTERS' | translate }} @@ -126,7 +126,7 @@ {{ 'ADDITIONAL_FILTERS' | translate }} @@ -134,7 +134,7 @@ {{ 'OBSERVATIONAL_FILTERS' | translate }} @@ -172,7 +172,7 @@ } From cc3773560cedd2bf6b26b4843d833d0b9bbb1161 Mon Sep 17 00:00:00 2001 From: Andrew Anderson Date: Mon, 1 Dec 2025 14:58:13 -0800 Subject: [PATCH 06/59] Lint space --- .../dataset-filters/dataset-filters.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/filters-dropdown/dataset-filters/dataset-filters.component.html b/src/app/components/filters-dropdown/dataset-filters/dataset-filters.component.html index fdc73c3e5..531e4554a 100644 --- a/src/app/components/filters-dropdown/dataset-filters/dataset-filters.component.html +++ b/src/app/components/filters-dropdown/dataset-filters/dataset-filters.component.html @@ -93,7 +93,7 @@ [collapsedHeight]="customCollapsedHeight" [expandedHeight]="customExpandedHeight" > - + {{ 'PRODUCT_FILTERS' | translate }} Date: Mon, 1 Dec 2025 14:02:52 -0900 Subject: [PATCH 07/59] feat: Update to use jobs patch endpoint --- .../scenes-list-header.component.ts | 23 +++------- src/app/services/hyp3/hyp3-api.service.ts | 43 +++++++++++++++++-- 2 files changed, 45 insertions(+), 21 deletions(-) diff --git a/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.ts b/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.ts index 45b584870..9266a6d9e 100644 --- a/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.ts +++ b/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.ts @@ -1,7 +1,7 @@ import { Component, OnDestroy, OnInit, inject } from '@angular/core'; import { saveAs } from 'file-saver'; -import { combineLatest, switchMap, from } from 'rxjs'; +import { combineLatest, switchMap } from 'rxjs'; import { debounceTime, filter, @@ -9,8 +9,6 @@ import { take, tap, withLatestFrom, - mergeMap, - toArray, } from 'rxjs/operators'; import { Store } from '@ngrx/store'; @@ -652,22 +650,11 @@ export class ScenesListHeaderComponent implements OnInit, OnDestroy { public onSubmitProjectName() { this.isEditingProjectName = false; - const newName = this.newProjectName; - - from(this.products).pipe( - mergeMap( - (product) => { - console.log(product.metadata.job.job_id); - return this.hyp3.updateJobName$(product.metadata.job.job_id, newName); - }, - 20 - ), - toArray(), - ).subscribe(resps => { - console.log(resps); - }); - console.log(`updating with ${this.newProjectName}`); + this.hyp3 + .updateJobsName$(this.products, this.newProjectName) + .subscribe(console.log); + this.newProjectName = ''; } diff --git a/src/app/services/hyp3/hyp3-api.service.ts b/src/app/services/hyp3/hyp3-api.service.ts index 49e099159..556e3c0d4 100644 --- a/src/app/services/hyp3/hyp3-api.service.ts +++ b/src/app/services/hyp3/hyp3-api.service.ts @@ -5,7 +5,8 @@ import { HttpParams, } from '@angular/common/http'; -import { Observable, of, first, catchError, map, forkJoin } from 'rxjs'; +import { Observable, of, first, catchError, map, forkJoin, from } from 'rxjs'; +import { mergeMap, toArray, bufferCount } from 'rxjs/operators'; import * as moment from 'moment'; import * as models from '@models'; @@ -157,13 +158,49 @@ export class Hyp3ApiService { ); } - public updateJobName$(jobId: string, projectName: string): Observable { + public updateJobName$( + jobId: string, + newProjectName: string, + ): Observable { const url = `${this.apiUrl}/jobs/${jobId}`; - return this.http.patch(url, { name: projectName }, { withCredentials: true }) + if (!newProjectName) { + newProjectName = null; + } + + return this.http + .patch( + url, + { name: newProjectName }, + { withCredentials: true }, + ) .pipe(map((resp) => resp as models.Hyp3Job)); } + public updateJobsName$( + products: models.CMRProduct[], + newProjectName: string, + ): Observable { + const url = `${this.apiUrl}/jobs`; + const jobIds = products.map((product) => product.metadata.job.job_id); + + if (!newProjectName) { + newProjectName = null; + } + + return from(jobIds).pipe( + bufferCount(100), + mergeMap((jobIdsBatch) => { + return this.http.patch( + url, + { name: newProjectName, job_ids: jobIdsBatch }, + { withCredentials: true }, + ); + }, 5), + toArray(), + ); + } + public submitJobBatch$(jobBatch: object) { const submitJobUrl = `${this.apiUrl}/jobs`; From 68ba0a631c7d64581b125cffbcd91e73b8233337 Mon Sep 17 00:00:00 2001 From: Andrew Anderson Date: Mon, 1 Dec 2025 15:19:05 -0800 Subject: [PATCH 08/59] Update src/app/components/filters-dropdown/dataset-filters/dataset-filters.component.scss Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../dataset-filters/dataset-filters.component.scss | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/app/components/filters-dropdown/dataset-filters/dataset-filters.component.scss b/src/app/components/filters-dropdown/dataset-filters/dataset-filters.component.scss index ecbcdb19f..f862ca5fa 100644 --- a/src/app/components/filters-dropdown/dataset-filters/dataset-filters.component.scss +++ b/src/app/components/filters-dropdown/dataset-filters/dataset-filters.component.scss @@ -14,10 +14,6 @@ justify-content: space-between; } -.info-icon { - margin-left: 10px; - margin-top: 4px; -} .info-text { flex: none; From a3d0e82c22dc7434f0fab1eb2bfd1f5ec7efb17b Mon Sep 17 00:00:00 2001 From: Andrew Anderson Date: Mon, 15 Dec 2025 15:45:17 -0800 Subject: [PATCH 09/59] Round 1 of revamping the UI --- .../scene-file/scene-file.component.html | 25 ++------- .../scene-file/scene-file.component.ts | 20 +++---- .../scene-file/scene-file.module.ts | 2 + .../shared/project-name-dialog/index.ts | 1 + .../project-name-dialog.component.html | 24 +++++++++ .../project-name-dialog.component.scss | 3 ++ .../project-name-dialog.component.ts | 52 +++++++++++++++++++ src/app/services/index.ts | 1 + .../services/project-name-dialog.service.ts | 26 ++++++++++ src/assets/i18n/de.json | 1 + src/assets/i18n/en.json | 1 + src/assets/i18n/es.json | 1 + 12 files changed, 124 insertions(+), 33 deletions(-) create mode 100644 src/app/components/shared/project-name-dialog/index.ts create mode 100644 src/app/components/shared/project-name-dialog/project-name-dialog.component.html create mode 100644 src/app/components/shared/project-name-dialog/project-name-dialog.component.scss create mode 100644 src/app/components/shared/project-name-dialog/project-name-dialog.component.ts create mode 100644 src/app/services/project-name-dialog.service.ts diff --git a/src/app/components/results-menu/scene-files/scene-file/scene-file.component.html b/src/app/components/results-menu/scene-files/scene-file/scene-file.component.html index 441ef26fc..476a558c3 100644 --- a/src/app/components/results-menu/scene-files/scene-file/scene-file.component.html +++ b/src/app/components/results-menu/scene-files/scene-file/scene-file.component.html @@ -137,27 +137,10 @@ @if (product.metadata.job) {
- {{ 'PROJECT_NAME' | translate }}: {{ product.metadata.job.name }} - - @if (!isEditingProjectName) { - - } - - @if (isEditingProjectName) { -
- - -
- } - - + {{ 'PROJECT_NAME' | translate }}: {{ product.metadata.job.name }} +
diff --git a/src/app/components/results-menu/scene-files/scene-file/scene-file.component.ts b/src/app/components/results-menu/scene-files/scene-file/scene-file.component.ts index 5e7448e5d..0ef411454 100644 --- a/src/app/components/results-menu/scene-files/scene-file/scene-file.component.ts +++ b/src/app/components/results-menu/scene-files/scene-file/scene-file.component.ts @@ -17,6 +17,7 @@ import { EnvironmentService, Hyp3JobStatusService, OnDemandService, + ProjectNameDialogService, } from '@services'; import * as models from '@models'; import { SubSink } from 'subsink'; @@ -38,6 +39,7 @@ export class SceneFileComponent implements OnInit, OnDestroy { private store$ = inject>(Store); env = inject(EnvironmentService); private onDemand = inject(OnDemandService); + private projectNameDialog = inject(ProjectNameDialogService); @Input() product: models.CMRProduct; @Input() isQueued: boolean; @@ -61,9 +63,6 @@ export class SceneFileComponent implements OnInit, OnDestroy { public paramsList = []; public copyIcons = models.CopyIcons; - public newProjectName = ''; - public isEditingProjectName = false; - private subs = new SubSink(); ngOnInit() { @@ -159,15 +158,12 @@ export class SceneFileComponent implements OnInit, OnDestroy { ); } - public onEditProjectName(oldProjectName: string) { - this.newProjectName = oldProjectName; - this.isEditingProjectName = true; - } - - public onSubmitProjectName() { - this.renameJobProjectName.emit(this.newProjectName); - this.isEditingProjectName = false; - this.newProjectName = ''; + public onEditProjectName(oldProjectName: string): void { + this.projectNameDialog.open(oldProjectName).subscribe((result) => { + if (result !== undefined) { + this.renameJobProjectName.emit(result); + } + }); } private expirationDays(expiration_time: moment.Moment): number { diff --git a/src/app/components/results-menu/scene-files/scene-file/scene-file.module.ts b/src/app/components/results-menu/scene-files/scene-file/scene-file.module.ts index 006679a57..07ac0446d 100644 --- a/src/app/components/results-menu/scene-files/scene-file/scene-file.module.ts +++ b/src/app/components/results-menu/scene-files/scene-file/scene-file.module.ts @@ -19,6 +19,7 @@ import { PipesModule } from '@pipes'; import { SceneFileComponent } from './scene-file.component'; import { DownloadFileButtonModule } from '@components/shared/download-file-button/download-file-button.module'; import { SharedModule } from '@shared'; +import { ProjectNameDialogComponent } from '@components/shared/project-name-dialog'; @NgModule({ declarations: [SceneFileComponent], @@ -36,6 +37,7 @@ import { SharedModule } from '@shared'; PipesModule, DownloadFileButtonModule, SharedModule, + ProjectNameDialogComponent, ], exports: [SceneFileComponent], }) diff --git a/src/app/components/shared/project-name-dialog/index.ts b/src/app/components/shared/project-name-dialog/index.ts new file mode 100644 index 000000000..77d2d9899 --- /dev/null +++ b/src/app/components/shared/project-name-dialog/index.ts @@ -0,0 +1 @@ +export { ProjectNameDialogComponent, ProjectNameDialogData } from './project-name-dialog.component'; diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.html b/src/app/components/shared/project-name-dialog/project-name-dialog.component.html new file mode 100644 index 000000000..257231bb8 --- /dev/null +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.html @@ -0,0 +1,24 @@ +

{{ 'EDIT_PROJECT_NAME' | translate }}

+ +
+ + + {{ 'PROJECT_NAME' | translate }} + + + + + + + + +
diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss b/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss new file mode 100644 index 000000000..ed6f86a69 --- /dev/null +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss @@ -0,0 +1,3 @@ +.project-name-field { + width: 100%; +} diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts new file mode 100644 index 000000000..71a47422b --- /dev/null +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts @@ -0,0 +1,52 @@ +import { Component, inject } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { + MatDialogRef, + MAT_DIALOG_DATA, + MatDialogTitle, + MatDialogContent, + MatDialogActions, +} from '@angular/material/dialog'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { MatButtonModule } from '@angular/material/button'; +import { TranslateModule } from '@ngx-translate/core'; + +export interface ProjectNameDialogData { + currentName: string; +} + +@Component({ + selector: 'app-project-name-dialog', + templateUrl: './project-name-dialog.component.html', + styleUrls: ['./project-name-dialog.component.scss'], + standalone: true, + imports: [ + FormsModule, + MatDialogTitle, + MatDialogContent, + MatDialogActions, + MatFormFieldModule, + MatInputModule, + MatButtonModule, + TranslateModule, + ], +}) +export class ProjectNameDialogComponent { + dialogRef = inject>(MatDialogRef); + data = inject(MAT_DIALOG_DATA); + + public projectName: string; + + constructor() { + this.projectName = this.data.currentName; + } + + public onCancel(): void { + this.dialogRef.close(); + } + + public onSave(): void { + this.dialogRef.close(this.projectName); + } +} diff --git a/src/app/services/index.ts b/src/app/services/index.ts index f8a99d5da..2b6018fff 100644 --- a/src/app/services/index.ts +++ b/src/app/services/index.ts @@ -43,3 +43,4 @@ export { ExportService } from './export.service'; export { NetcdfService } from './netcdf-service.service'; export { PointHistoryService } from './point-history.service'; export { FrameMapService } from './frame-map.service'; +export { ProjectNameDialogService } from './project-name-dialog.service'; diff --git a/src/app/services/project-name-dialog.service.ts b/src/app/services/project-name-dialog.service.ts new file mode 100644 index 000000000..dbef2469e --- /dev/null +++ b/src/app/services/project-name-dialog.service.ts @@ -0,0 +1,26 @@ +import { Injectable, inject } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { Observable } from 'rxjs'; + +import { + ProjectNameDialogComponent, + ProjectNameDialogData, +} from '@components/shared/project-name-dialog'; + +@Injectable({ providedIn: 'root' }) +export class ProjectNameDialogService { + private dialog = inject(MatDialog); + + open(currentName: string): Observable { + const dialogRef = this.dialog.open< + ProjectNameDialogComponent, + ProjectNameDialogData, + string + >(ProjectNameDialogComponent, { + width: '400px', + data: { currentName }, + }); + + return dialogRef.afterClosed(); + } +} diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json index b40865ea1..8784dec9c 100644 --- a/src/assets/i18n/de.json +++ b/src/assets/i18n/de.json @@ -230,6 +230,7 @@ "EDIT_FILTERS": "Filter bearbeiten", "EDIT_LIST": "Liste bearbeiten", "EDIT_NAME": "Name bearbeiten", + "EDIT_PROJECT_NAME": "Projektname bearbeiten", "EMAIL": "E-Mail", "END": "Ende", "END_DATE": "Enddatum", diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 642365d9e..3c4cd8e3c 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -283,6 +283,7 @@ "EDIT_FILTERS": "Edit Filters", "EDIT_LIST": "Edit List", "EDIT_NAME": "Edit name", + "EDIT_PROJECT_NAME": "Edit Project Name", "EMAIL": "email", "END": "End", "END_DATE": "End Date", diff --git a/src/assets/i18n/es.json b/src/assets/i18n/es.json index 7dab2030e..9e1c5edb7 100644 --- a/src/assets/i18n/es.json +++ b/src/assets/i18n/es.json @@ -281,6 +281,7 @@ "EDIT_FILTERS": "Editar filtros", "EDIT_LIST": "Lista de edición", "EDIT_NAME": "Editar nombre", + "EDIT_PROJECT_NAME": "Editar Nombre del Proyecto", "EMAIL": "correo electrónico", "END": "Fin", "END_DATE": "Fecha Final", From b8d6137f239c24720a126ac0e181196d5bfdce90 Mon Sep 17 00:00:00 2001 From: Andrew Anderson Date: Mon, 15 Dec 2025 16:06:31 -0800 Subject: [PATCH 10/59] Fix console error related to translation. --- .../project-name-dialog/project-name-dialog.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts index 71a47422b..81ca0399c 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts @@ -10,7 +10,7 @@ import { import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatButtonModule } from '@angular/material/button'; -import { TranslateModule } from '@ngx-translate/core'; +import { SharedModule } from '@shared'; export interface ProjectNameDialogData { currentName: string; @@ -29,7 +29,7 @@ export interface ProjectNameDialogData { MatFormFieldModule, MatInputModule, MatButtonModule, - TranslateModule, + SharedModule, ], }) export class ProjectNameDialogComponent { From 943ef25d8be8c8fc9b952fb095ced778e6165934 Mon Sep 17 00:00:00 2001 From: Andrew Anderson Date: Mon, 15 Dec 2025 16:15:04 -0800 Subject: [PATCH 11/59] Bulk rename uses new dialog --- .../scenes-list-header.component.html | 14 ----------- .../scenes-list-header.component.ts | 24 +++++++------------ 2 files changed, 8 insertions(+), 30 deletions(-) diff --git a/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.html b/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.html index f4ed610d1..b1d13a9f3 100644 --- a/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.html +++ b/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.html @@ -322,7 +322,6 @@
- @if (!isEditingProjectName) {
@@ -353,19 +352,6 @@
- } - } - - @if (isEditingProjectName) { -
- - -
} diff --git a/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.ts b/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.ts index 9266a6d9e..b5c832935 100644 --- a/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.ts +++ b/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.ts @@ -32,6 +32,7 @@ import { SarviewsEventsService, NotificationService, ExportService, + ProjectNameDialogService, } from '@services'; import * as models from '@models'; @@ -66,6 +67,7 @@ export class ScenesListHeaderComponent implements OnInit, OnDestroy { private possibleHyp3JobsService = inject(PossibleHyp3JobsService); private exportService = inject(ExportService); private dialog = inject(MatDialog); + private projectNameDialog = inject(ProjectNameDialogService); public copyIcon = faCopy; public pairs$ = this.pairService.pairs$; @@ -272,9 +274,6 @@ export class ScenesListHeaderComponent implements OnInit, OnDestroy { private selectedEvent: models.SarviewsEvent; - public isEditingProjectName = false; - public newProjectName = ''; - ngOnInit() { this.subs.add( this.pairProducts$.subscribe((products) => { @@ -643,19 +642,12 @@ export class ScenesListHeaderComponent implements OnInit, OnDestroy { }); } - public onBulkProjectNameUpdate() { - this.isEditingProjectName = true; - this.newProjectName = ''; - } - - public onSubmitProjectName() { - this.isEditingProjectName = false; - - this.hyp3 - .updateJobsName$(this.products, this.newProjectName) - .subscribe(console.log); - - this.newProjectName = ''; + public onBulkProjectNameUpdate(): void { + this.projectNameDialog.open('').subscribe((result) => { + if (result !== undefined) { + this.hyp3.updateJobsName$(this.products, result).subscribe(console.log); + } + }); } ngOnDestroy(): void { From 9eac503b9de904740709fb36091d81fd161bf949 Mon Sep 17 00:00:00 2001 From: Andrew Anderson Date: Mon, 15 Dec 2025 16:21:04 -0800 Subject: [PATCH 12/59] npm run lint -- --fix --- .../header/info-bar/info-bar.component.html | 9 ++++--- .../view-selector/view-selector.component.ts | 7 ++++-- .../scene-file/scene-file.component.html | 5 +++- .../scene-files/scene-files.component.ts | 10 ++++++-- .../scenes-list-header.component.html | 3 +-- .../shared/project-name-dialog/index.ts | 5 +++- .../product-science-selector.component.html | 9 +++---- .../product-science-selector.component.ts | 5 +++- .../season-selector.component.ts | 3 ++- src/karma.conf.js | 24 +++++++++---------- 10 files changed, 51 insertions(+), 29 deletions(-) diff --git a/src/app/components/header/info-bar/info-bar.component.html b/src/app/components/header/info-bar/info-bar.component.html index f24bb31f9..187dd02f0 100644 --- a/src/app/components/header/info-bar/info-bar.component.html +++ b/src/app/components/header/info-bar/info-bar.component.html @@ -23,9 +23,12 @@ @if ((searchType$ | async) === searchTypes.DISPLACEMENT) { - - - + + + } {{ 'START' | translate }}: {{ startDate | shortDate }} diff --git a/src/app/components/map/map-controls/view-selector/view-selector.component.ts b/src/app/components/map/map-controls/view-selector/view-selector.component.ts index 7f0d1b008..dfe846261 100644 --- a/src/app/components/map/map-controls/view-selector/view-selector.component.ts +++ b/src/app/components/map/map-controls/view-selector/view-selector.component.ts @@ -46,8 +46,11 @@ export class ViewSelectorComponent implements OnInit, OnDestroy { this.isDisplacementSearch = searchType === SearchType.DISPLACEMENT; // Auto-switch to equatorial view if switching to Displacement Search from a polar view - if (this.isDisplacementSearch && - (this.view === MapViewType.ARCTIC || this.view === MapViewType.ANTARCTIC)) { + if ( + this.isDisplacementSearch && + (this.view === MapViewType.ARCTIC || + this.view === MapViewType.ANTARCTIC) + ) { this.store$.dispatch(new mapStore.SetMapView(MapViewType.EQUATORIAL)); } }), diff --git a/src/app/components/results-menu/scene-files/scene-file/scene-file.component.html b/src/app/components/results-menu/scene-files/scene-file/scene-file.component.html index 476a558c3..4366f903f 100644 --- a/src/app/components/results-menu/scene-files/scene-file/scene-file.component.html +++ b/src/app/components/results-menu/scene-files/scene-file/scene-file.component.html @@ -138,7 +138,10 @@
{{ 'PROJECT_NAME' | translate }}: {{ product.metadata.job.name }} -
diff --git a/src/app/components/results-menu/scene-files/scene-files.component.ts b/src/app/components/results-menu/scene-files/scene-files.component.ts index 1203a54a8..78dbbb069 100644 --- a/src/app/components/results-menu/scene-files/scene-files.component.ts +++ b/src/app/components/results-menu/scene-files/scene-files.component.ts @@ -328,10 +328,16 @@ export class SceneFilesComponent this.store$.dispatch(new queueStore.AddJob(job)); } - public onRenameJobProjectName(product: models.CMRProduct, newProjectName: string) { + public onRenameJobProjectName( + product: models.CMRProduct, + newProjectName: string, + ) { const job = product.metadata.job; this.hyp3.updateJobName$(job.job_id, newProjectName).subscribe((job) => { - const action = new scenesStore.UpdateProductWithNewProjectName({ productId: product.id, name: job.name }); + const action = new scenesStore.UpdateProductWithNewProjectName({ + productId: product.id, + name: job.name, + }); this.store$.dispatch(action); }); } diff --git a/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.html b/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.html index b1d13a9f3..d62968f74 100644 --- a/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.html +++ b/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.html @@ -348,13 +348,12 @@
} -
@for (sciProd of group.sciProd; track sciProd) { - {{ - sciProd.viewValue - }} + {{ sciProd.viewValue }} } } } - *{{ 'RECOMMENDED_PRODUCT' | translate }} + *{{ 'RECOMMENDED_PRODUCT' | translate }}
diff --git a/src/app/components/shared/selectors/product-science-selector/product-science-selector.component.ts b/src/app/components/shared/selectors/product-science-selector/product-science-selector.component.ts index aa76a8ec9..a4a7d4006 100644 --- a/src/app/components/shared/selectors/product-science-selector/product-science-selector.component.ts +++ b/src/app/components/shared/selectors/product-science-selector/product-science-selector.component.ts @@ -51,7 +51,10 @@ export class ProductScienceSelectorComponent implements OnInit, OnDestroy { disabled: false, sciProd: [ { value: 'GSLC', viewValue: 'GSLC (Geocoded Single Look Complex)' }, - { value: 'GCOV', viewValue: '*GCOV (Geocoded Polarimetric Covariance)' }, + { + value: 'GCOV', + viewValue: '*GCOV (Geocoded Polarimetric Covariance)', + }, { value: 'GUNW', viewValue: 'GUNW (Geocoded Interferogram)' }, { value: 'GOFF', viewValue: 'GOFF (Geocoded Pixel Offsets)' }, ], diff --git a/src/app/components/shared/selectors/season-selector/season-selector.component.ts b/src/app/components/shared/selectors/season-selector/season-selector.component.ts index ac929eaa7..5ecfae8f4 100644 --- a/src/app/components/shared/selectors/season-selector/season-selector.component.ts +++ b/src/app/components/shared/selectors/season-selector/season-selector.component.ts @@ -64,7 +64,8 @@ export class SeasonSelectorComponent implements OnInit, OnDestroy { setTimeout(() => { this.store$.dispatch(new filtersStore.SetSeasonStart(1)); this.store$.dispatch(new filtersStore.SetSeasonEnd(180)); - }, 0); } + }, 0); + } } public onSeasonStartChange(dayOfYear: number): void { diff --git a/src/karma.conf.js b/src/karma.conf.js index 44bf3b5fc..e85f053b9 100644 --- a/src/karma.conf.js +++ b/src/karma.conf.js @@ -16,20 +16,20 @@ module.exports = function (config) { autoWatch: true, browsers: ['ChromeHeadless'], customLaunchers: { - HeadlessChrome: { - base: 'ChromeHeadless', - flags: [ - '--no-sandbox', - '--headless', - '--disable-setuid-sandbox', - '--disable-gpu', - '--disable-translate', - '--disable-extensions' - ] - } + HeadlessChrome: { + base: 'ChromeHeadless', + flags: [ + '--no-sandbox', + '--headless', + '--disable-setuid-sandbox', + '--disable-gpu', + '--disable-translate', + '--disable-extensions', + ], + }, }, singleRun: false, browserNoActivityTimeout: 120000, - urlRoot: '' + urlRoot: '', }); }; From dcf6b046291111eea8f85f27bcabc430b24f2fff Mon Sep 17 00:00:00 2001 From: Andrew Anderson Date: Wed, 17 Dec 2025 11:38:23 -0800 Subject: [PATCH 13/59] On Demand search reloads after bulk rename --- .../scenes-list-header.component.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.ts b/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.ts index b5c832935..5a39c1a04 100644 --- a/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.ts +++ b/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.ts @@ -5,6 +5,7 @@ import { combineLatest, switchMap } from 'rxjs'; import { debounceTime, filter, + finalize, map, take, tap, @@ -645,7 +646,15 @@ export class ScenesListHeaderComponent implements OnInit, OnDestroy { public onBulkProjectNameUpdate(): void { this.projectNameDialog.open('').subscribe((result) => { if (result !== undefined) { - this.hyp3.updateJobsName$(this.products, result).subscribe(console.log); + this.hyp3 + .updateJobsName$(this.products, result) + .pipe( + finalize(() => { + this.store$.dispatch(new scenesStore.ClearScenes()); + this.store$.dispatch(new searchStore.MakeSearch()); + }), + ) + .subscribe(); } }); } From 79a9498c7c3babeacab3170b7ea495061dcc5e7b Mon Sep 17 00:00:00 2001 From: Andrew Anderson Date: Wed, 17 Dec 2025 12:12:37 -0800 Subject: [PATCH 14/59] npm run lint -- --fix --- .../product-science-selector.component.html | 9 +++---- .../product-science-selector.component.ts | 5 +++- src/karma.conf.js | 24 +++++++++---------- 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/app/components/shared/selectors/product-science-selector/product-science-selector.component.html b/src/app/components/shared/selectors/product-science-selector/product-science-selector.component.html index 3df3a4dd0..f21d18cef 100644 --- a/src/app/components/shared/selectors/product-science-selector/product-science-selector.component.html +++ b/src/app/components/shared/selectors/product-science-selector/product-science-selector.component.html @@ -17,15 +17,16 @@ class="sci-prod-optgroup" > @for (sciProd of group.sciProd; track sciProd) { - {{ - sciProd.viewValue - }} + {{ sciProd.viewValue }} } } } - *{{ 'RECOMMENDED_PRODUCT' | translate }} + *{{ 'RECOMMENDED_PRODUCT' | translate }}
diff --git a/src/app/components/shared/selectors/product-science-selector/product-science-selector.component.ts b/src/app/components/shared/selectors/product-science-selector/product-science-selector.component.ts index 734f06824..18cab8aa8 100644 --- a/src/app/components/shared/selectors/product-science-selector/product-science-selector.component.ts +++ b/src/app/components/shared/selectors/product-science-selector/product-science-selector.component.ts @@ -51,7 +51,10 @@ export class ProductScienceSelectorComponent implements OnInit, OnDestroy { disabled: false, sciProd: [ { value: 'GSLC', viewValue: 'GSLC (Geocoded Single Look Complex)' }, - { value: 'GCOV', viewValue: '*GCOV (Geocoded Polarimetric Covariance)' }, + { + value: 'GCOV', + viewValue: '*GCOV (Geocoded Polarimetric Covariance)', + }, { value: 'GUNW', viewValue: 'GUNW (Geocoded Interferogram)' }, { value: 'GOFF', viewValue: 'GOFF (Geocoded Pixel Offsets)' }, ], diff --git a/src/karma.conf.js b/src/karma.conf.js index 44bf3b5fc..e85f053b9 100644 --- a/src/karma.conf.js +++ b/src/karma.conf.js @@ -16,20 +16,20 @@ module.exports = function (config) { autoWatch: true, browsers: ['ChromeHeadless'], customLaunchers: { - HeadlessChrome: { - base: 'ChromeHeadless', - flags: [ - '--no-sandbox', - '--headless', - '--disable-setuid-sandbox', - '--disable-gpu', - '--disable-translate', - '--disable-extensions' - ] - } + HeadlessChrome: { + base: 'ChromeHeadless', + flags: [ + '--no-sandbox', + '--headless', + '--disable-setuid-sandbox', + '--disable-gpu', + '--disable-translate', + '--disable-extensions', + ], + }, }, singleRun: false, browserNoActivityTimeout: 120000, - urlRoot: '' + urlRoot: '', }); }; From e40213a1d2eeb83232ad84166a67d41e3630d5bc Mon Sep 17 00:00:00 2001 From: Andrew Anderson Date: Wed, 17 Dec 2025 12:59:27 -0800 Subject: [PATCH 15/59] Resolving issues from merging with test --- .../scene-file/scene-file.component.html | 13 +++--- .../scenes-list-header.component.html | 42 +++++++++++-------- .../project-name-dialog.component.ts | 4 +- 3 files changed, 33 insertions(+), 26 deletions(-) diff --git a/src/app/components/results-menu/scene-files/scene-file/scene-file.component.html b/src/app/components/results-menu/scene-files/scene-file/scene-file.component.html index 81069309d..21b8b2867 100644 --- a/src/app/components/results-menu/scene-files/scene-file/scene-file.component.html +++ b/src/app/components/results-menu/scene-files/scene-file/scene-file.component.html @@ -151,14 +151,13 @@
{{ 'PROJECT_NAME' | translate }}: {{ product.metadata.job.name }} +
- -
}
diff --git a/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.html b/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.html index 4b98c4f98..4d3300f30 100644 --- a/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.html +++ b/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.html @@ -311,27 +311,35 @@
-
- -
-
-
- - + +
+
+
+ - edit - - + + edit + + +
+ + +
+
+
} @if (searchType === SearchTypes.BASELINE) {
diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts index 81ca0399c..71a47422b 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts @@ -10,7 +10,7 @@ import { import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatButtonModule } from '@angular/material/button'; -import { SharedModule } from '@shared'; +import { TranslateModule } from '@ngx-translate/core'; export interface ProjectNameDialogData { currentName: string; @@ -29,7 +29,7 @@ export interface ProjectNameDialogData { MatFormFieldModule, MatInputModule, MatButtonModule, - SharedModule, + TranslateModule, ], }) export class ProjectNameDialogComponent { From 03363eb98ffa6ddb32384a8597e4d1f6ba314c57 Mon Sep 17 00:00:00 2001 From: Andrew Anderson Date: Wed, 17 Dec 2025 13:15:52 -0800 Subject: [PATCH 16/59] Snackbar message for renaming jobs --- .../results-menu/scene-files/scene-files.component.html | 4 ++++ .../scenes-list-header/scenes-list-header.component.ts | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/src/app/components/results-menu/scene-files/scene-files.component.html b/src/app/components/results-menu/scene-files/scene-files.component.html index 0eefa4efb..72491bb0e 100644 --- a/src/app/components/results-menu/scene-files/scene-files.component.html +++ b/src/app/components/results-menu/scene-files/scene-files.component.html @@ -19,6 +19,7 @@ (toggle)="onToggleQueueProduct(product)" (unzip)="onOpenUnzipProduct($event)" (queueHyp3Job)="onQueueHyp3Job($event)" + (renameJobProjectName)="onRenameJobProjectName(product, $event)" [loadingHyp3JobName]="loadingHyp3JobName" [isUserLoggedIn]="isUserLoggedIn" [product]="product" @@ -35,6 +36,7 @@ (toggle)="onToggleQueueProduct(product)" (unzip)="onOpenUnzipProduct($event)" (queueHyp3Job)="onQueueHyp3Job($event)" + (renameJobProjectName)="onRenameJobProjectName(product, $event)" [loadingHyp3JobName]="loadingHyp3JobName" [isUserLoggedIn]="isUserLoggedIn" [product]="product" @@ -149,6 +151,7 @@ (unzip)="onOpenUnzipProduct($event)" (closeProduct)="onCloseProduct($event)" (queueHyp3Job)="onQueueHyp3Job($event)" + (renameJobProjectName)="onRenameJobProjectName(product, $event)" [isUserLoggedIn]="isUserLoggedIn" [product]="product" [validHyp3JobTypes]="validJobTypesByProduct[product.id]" @@ -171,6 +174,7 @@ (unzip)="onOpenUnzipProduct($event)" (closeProduct)="onCloseProduct($event)" (queueHyp3Job)="onQueueHyp3Job($event)" + (renameJobProjectName)="onRenameJobProjectName(product, $event)" [isUserLoggedIn]="isUserLoggedIn" [product]="product" [loadingHyp3JobName]="loadingHyp3JobName" diff --git a/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.ts b/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.ts index ec64c4635..b22828274 100644 --- a/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.ts +++ b/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.ts @@ -47,6 +47,7 @@ import { CodeExportType, } from '@components/shared/code-export'; import { MatDialog } from '@angular/material/dialog'; +import { MatSnackBar } from '@angular/material/snack-bar'; import { NgClass, AsyncPipe, @@ -106,6 +107,7 @@ export class ScenesListHeaderComponent implements OnInit, OnDestroy { private possibleHyp3JobsService = inject(PossibleHyp3JobsService); private exportService = inject(ExportService); private dialog = inject(MatDialog); + private snackBar = inject(MatSnackBar); private projectNameDialog = inject(ProjectNameDialogService); public copyIcon = faCopy; @@ -684,10 +686,16 @@ export class ScenesListHeaderComponent implements OnInit, OnDestroy { public onBulkProjectNameUpdate(): void { this.projectNameDialog.open('').subscribe((result) => { if (result !== undefined) { + this.snackBar.open('Renaming jobs...', '', { + horizontalPosition: 'center', + verticalPosition: 'bottom', + }); + this.hyp3 .updateJobsName$(this.products, result) .pipe( finalize(() => { + this.snackBar.dismiss(); this.store$.dispatch(new scenesStore.ClearScenes()); this.store$.dispatch(new searchStore.MakeSearch()); }), From e723faa34c07be31b52cfdcce6ae7f29908a9a33 Mon Sep 17 00:00:00 2001 From: Andrew Anderson Date: Wed, 17 Dec 2025 13:58:37 -0800 Subject: [PATCH 17/59] Adding multi-lingual support to rename dialog And a few other UI tweaks. --- .gitignore | 1 + .../scenes-list-header.component.html | 11 +++-- .../scenes-list-header.component.ts | 46 +++++++++++-------- .../project-name-dialog.component.html | 9 +++- .../project-name-dialog.component.scss | 12 +++++ .../project-name-dialog.component.ts | 1 + .../services/project-name-dialog.service.ts | 4 +- src/assets/i18n/de.json | 6 +++ src/assets/i18n/en.json | 6 +++ src/assets/i18n/es.json | 6 +++ 10 files changed, 77 insertions(+), 25 deletions(-) diff --git a/.gitignore b/.gitignore index 1dc9b1154..d8cb675af 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,4 @@ local-serve.sh # Claude Code .claude/ CLAUDE.md +TRANSLATIONS_SCRATCH.md diff --git a/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.html b/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.html index 4d3300f30..94a5cd52c 100644 --- a/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.html +++ b/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.html @@ -312,7 +312,7 @@
- +
@@ -324,7 +324,9 @@ edit @@ -336,7 +338,10 @@
diff --git a/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.ts b/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.ts index b22828274..645e39fa8 100644 --- a/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.ts +++ b/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.ts @@ -34,6 +34,7 @@ import { NotificationService, ExportService, ProjectNameDialogService, + AsfLanguageService, } from '@services'; import * as models from '@models'; @@ -109,6 +110,7 @@ export class ScenesListHeaderComponent implements OnInit, OnDestroy { private dialog = inject(MatDialog); private snackBar = inject(MatSnackBar); private projectNameDialog = inject(ProjectNameDialogService); + private language = inject(AsfLanguageService); public copyIcon = faCopy; public pairs$ = this.pairService.pairs$; @@ -684,25 +686,31 @@ export class ScenesListHeaderComponent implements OnInit, OnDestroy { } public onBulkProjectNameUpdate(): void { - this.projectNameDialog.open('').subscribe((result) => { - if (result !== undefined) { - this.snackBar.open('Renaming jobs...', '', { - horizontalPosition: 'center', - verticalPosition: 'bottom', - }); - - this.hyp3 - .updateJobsName$(this.products, result) - .pipe( - finalize(() => { - this.snackBar.dismiss(); - this.store$.dispatch(new scenesStore.ClearScenes()); - this.store$.dispatch(new searchStore.MakeSearch()); - }), - ) - .subscribe(); - } - }); + this.projectNameDialog + .open('', this.products.length) + .subscribe((result) => { + if (result !== undefined) { + this.snackBar.open( + this.language.translate.instant('RENAMING_JOBS'), + '', + { + horizontalPosition: 'center', + verticalPosition: 'bottom', + }, + ); + + this.hyp3 + .updateJobsName$(this.products, result) + .pipe( + finalize(() => { + this.snackBar.dismiss(); + this.store$.dispatch(new scenesStore.ClearScenes()); + this.store$.dispatch(new searchStore.MakeSearch()); + }), + ) + .subscribe(); + } + }); } ngOnDestroy(): void { diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.html b/src/app/components/shared/project-name-dialog/project-name-dialog.component.html index 257231bb8..56c35aa3f 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.html +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.html @@ -1,7 +1,14 @@ -

{{ 'EDIT_PROJECT_NAME' | translate }}

+

+ {{ (data.jobCount > 1 ? 'EDIT_ALL_PROJECT_NAMES' : 'EDIT_PROJECT_NAME') | translate }} +

+ @if (data.jobCount) { +

+ {{ 'WILL_RENAME_JOBS' | translate: { count: data.jobCount } }} +

+ } {{ 'PROJECT_NAME' | translate }} { + open(currentName: string, jobCount?: number): Observable { const dialogRef = this.dialog.open< ProjectNameDialogComponent, ProjectNameDialogData, string >(ProjectNameDialogComponent, { width: '400px', - data: { currentName }, + data: { currentName, jobCount }, }); return dialogRef.afterClosed(); diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json index 8784dec9c..b493c816a 100644 --- a/src/assets/i18n/de.json +++ b/src/assets/i18n/de.json @@ -230,6 +230,7 @@ "EDIT_FILTERS": "Filter bearbeiten", "EDIT_LIST": "Liste bearbeiten", "EDIT_NAME": "Name bearbeiten", + "EDIT_ALL_PROJECT_NAMES": "Alle Projektnamen bearbeiten", "EDIT_PROJECT_NAME": "Projektname bearbeiten", "EMAIL": "E-Mail", "END": "Ende", @@ -556,7 +557,10 @@ "PRODUCTS_IN_YOUR_QUEUE_MAY_REQUIRE_A_RESTRICTED_DATASET_AGREEMENT": "Für Produkte in Ihrer Warteschlange ist möglicherweise eine Vereinbarung mit eingeschränkten Datensätzen erforderlich.", "PRODUCTS_TO_DOWNLOADS_FROM_SELECTED_EVENT": "Produkte zum Download von ausgewählten Veranstaltungen", "PROFILE": "Profil", + "PROJECT": "Projekt", "PROJECT_NAME": "Projektname", + "PROJECT_NAME_UPDATE_ALL_TOOLTIP": "Projektname für alle geladenen Aufträge aktualisieren", + "PROJECT_NAME_UPDATE_FOR_JOBS": "Projektname für {{count}} Aufträge aktualisieren", "PUSH_PIN": "push_pin", "PYTHON_PY": "Python (.py)", "QUAKE": "beben", @@ -591,6 +595,8 @@ "RESTRICTED_DATA_USE_AGREEMENT": "Vereinbarung zur eingeschränkten Datennutzung", "RESUBMIT_JOB": "Erneutes Einreichen des Auftrags", "RESUBMIT_PROJECT": "Projekt erneut einreichen", + "RENAMING_JOBS": "Aufträge werden umbenannt...", + "WILL_RENAME_JOBS": "{{count}} Auftrag/Aufträge werden umbenannt.", "RESULTS_TO_ON_DEMAND_QUEUE": "Ergebnisse in die On-Demand-Warteschlange", "RESULTS_WILL_NOW_CONTAIN_ONLY_SCENES_WITH_THE_SAME_PATH_AND_FRAME": "Die Ergebnisse enthalten jetzt nur noch Szenen mit demselben Pfad und Frame.", "REVIEW": "Rezension", diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index e83a881d3..a994143b3 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -283,6 +283,7 @@ "EDIT_FILTERS": "Edit Filters", "EDIT_LIST": "Edit List", "EDIT_NAME": "Edit name", + "EDIT_ALL_PROJECT_NAMES": "Edit All Project Names", "EDIT_PROJECT_NAME": "Edit Project Name", "EMAIL": "email", "END": "End", @@ -667,7 +668,10 @@ "PRODUCTS_IN_YOUR_QUEUE_MAY_REQUIRE_A_RESTRICTED_DATASET_AGREEMENT": "Products in your queue may require a restricted dataset agreement.", "PRODUCTS_TO_DOWNLOADS_FROM_SELECTED_EVENT": "products to downloads from selected event", "PROFILE": "Profile", + "PROJECT": "Project", "PROJECT_NAME": "Project Name", + "PROJECT_NAME_UPDATE_ALL_TOOLTIP": "Update project name for all loaded jobs", + "PROJECT_NAME_UPDATE_FOR_JOBS": "Update project name for {{count}} jobs", "PUSH_PIN": "push_pin", "PYTHON_PY": "python (.py)", "QUAKE": "quake", @@ -710,6 +714,8 @@ "RESTRICTED_DATA_USE_AGREEMENT": "Restricted Data Use Agreement", "RESUBMIT_JOB": "Resubmit Job", "RESUBMIT_PROJECT": "Resubmit Project", + "RENAMING_JOBS": "Renaming jobs...", + "WILL_RENAME_JOBS": "This will rename {{count}} job(s).", "RESULTS_TO_ON_DEMAND_QUEUE": "results to On Demand queue", "RESULTS_WILL_NOW_CONTAIN_ONLY_SCENES_WITH_THE_SAME_PATH_AND_FRAME": "results will now contain only scenes with the same path and frame.", "REVIEW": "Review", diff --git a/src/assets/i18n/es.json b/src/assets/i18n/es.json index 9d994a429..4603435fe 100644 --- a/src/assets/i18n/es.json +++ b/src/assets/i18n/es.json @@ -283,6 +283,7 @@ "EDIT_FILTERS": "Editar filtros", "EDIT_LIST": "Lista de edición", "EDIT_NAME": "Editar nombre", + "EDIT_ALL_PROJECT_NAMES": "Editar Todos los Nombres de Proyecto", "EDIT_PROJECT_NAME": "Editar Nombre del Proyecto", "EMAIL": "correo electrónico", "END": "Fin", @@ -668,7 +669,10 @@ "PRODUCTS_IN_YOUR_QUEUE_MAY_REQUIRE_A_RESTRICTED_DATASET_AGREEMENT": "Los productos en su lista pueden requerir un acuerdo de conjunto de datos restringido.", "PRODUCTS_TO_DOWNLOADS_FROM_SELECTED_EVENT": "los productos a las descargas del evento seleccionado", "PROFILE": "Perfil", + "PROJECT": "Proyecto", "PROJECT_NAME": "Nombre del Proyecto", + "PROJECT_NAME_UPDATE_ALL_TOOLTIP": "Actualizar nombre del proyecto para todos los trabajos cargados", + "PROJECT_NAME_UPDATE_FOR_JOBS": "Actualizar nombre del proyecto para {{count}} trabajos", "PUSH_PIN": "chincheta", "PYTHON_PY": "python (.py)", "QUAKE": "terremoto", @@ -711,6 +715,8 @@ "RESTRICTED_DATA_USE_AGREEMENT": "Acuerdo de Uso de Datos Restringidos", "RESUBMIT_JOB": "Reenviar trabajo", "RESUBMIT_PROJECT": "Reenviar Proyecto", + "RENAMING_JOBS": "Renombrando trabajos...", + "WILL_RENAME_JOBS": "Se renombrarán {{count}} trabajo(s).", "RESULTS_TO_ON_DEMAND_QUEUE": "resultados a la lista On Demand", "RESULTS_WILL_NOW_CONTAIN_ONLY_SCENES_WITH_THE_SAME_PATH_AND_FRAME": "los resultados ahora contendrán solo escenas con la misma ruta y marco.", "REVIEW": "Revisar", From e718b89a428b154587d217c809b6fa938e7cfbe0 Mon Sep 17 00:00:00 2001 From: Andrew Anderson Date: Wed, 17 Dec 2025 14:20:22 -0800 Subject: [PATCH 18/59] Edit Validation Added checks for empty project name entry or only spaces. Also validate that the jobs were successfully renamed and provided error handling. Will provide error notification with toast. --- .../scenes-list-header.component.ts | 46 ++++++++++++++----- .../project-name-dialog.component.html | 12 +++-- .../project-name-dialog.component.ts | 8 +++- src/app/services/hyp3/hyp3-api.service.ts | 26 ++++++++--- src/app/services/notification.service.ts | 11 +++++ src/assets/i18n/de.json | 4 ++ src/assets/i18n/en.json | 4 ++ src/assets/i18n/es.json | 4 ++ 8 files changed, 94 insertions(+), 21 deletions(-) diff --git a/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.ts b/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.ts index 645e39fa8..a626a4475 100644 --- a/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.ts +++ b/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.ts @@ -5,7 +5,6 @@ import { combineLatest, switchMap } from 'rxjs'; import { debounceTime, filter, - finalize, map, take, tap, @@ -699,16 +698,41 @@ export class ScenesListHeaderComponent implements OnInit, OnDestroy { }, ); - this.hyp3 - .updateJobsName$(this.products, result) - .pipe( - finalize(() => { - this.snackBar.dismiss(); - this.store$.dispatch(new scenesStore.ClearScenes()); - this.store$.dispatch(new searchStore.MakeSearch()); - }), - ) - .subscribe(); + this.hyp3.updateJobsName$(this.products, result).subscribe({ + next: ({ success, failed }) => { + this.snackBar.dismiss(); + + if (failed === 0) { + this.notificationService.info( + this.language.translate.instant('RENAME_SUCCESS', { + count: success, + }), + ); + } else if (success === 0) { + this.notificationService.error( + this.language.translate.instant('RENAME_ALL_FAILED', { + count: failed, + }), + ); + } else { + this.notificationService.warn( + this.language.translate.instant('RENAME_PARTIAL_SUCCESS', { + success, + failed, + }), + ); + } + + this.store$.dispatch(new scenesStore.ClearScenes()); + this.store$.dispatch(new searchStore.MakeSearch()); + }, + error: () => { + this.snackBar.dismiss(); + this.notificationService.error( + this.language.translate.instant('RENAME_ERROR'), + ); + }, + }); } }); } diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.html b/src/app/components/shared/project-name-dialog/project-name-dialog.component.html index 56c35aa3f..a60dbcf03 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.html +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.html @@ -1,11 +1,17 @@

- {{ (data.jobCount > 1 ? 'EDIT_ALL_PROJECT_NAMES' : 'EDIT_PROJECT_NAME') | translate }} + {{ + (data.jobCount > 1 ? 'EDIT_ALL_PROJECT_NAMES' : 'EDIT_PROJECT_NAME') + | translate + }}

@if (data.jobCount) { -

+

{{ 'WILL_RENAME_JOBS' | translate: { count: data.jobCount } }}

} @@ -24,7 +30,7 @@

- diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts index 19e6cdab2..352c4fdaf 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts @@ -43,11 +43,17 @@ export class ProjectNameDialogComponent { this.projectName = this.data.currentName; } + public get isValid(): boolean { + return this.projectName?.trim().length > 0; + } + public onCancel(): void { this.dialogRef.close(); } public onSave(): void { - this.dialogRef.close(this.projectName); + if (this.isValid) { + this.dialogRef.close(this.projectName.trim()); + } } } diff --git a/src/app/services/hyp3/hyp3-api.service.ts b/src/app/services/hyp3/hyp3-api.service.ts index 556e3c0d4..8b6e0ba4b 100644 --- a/src/app/services/hyp3/hyp3-api.service.ts +++ b/src/app/services/hyp3/hyp3-api.service.ts @@ -180,7 +180,7 @@ export class Hyp3ApiService { public updateJobsName$( products: models.CMRProduct[], newProjectName: string, - ): Observable { + ): Observable<{ success: number; failed: number }> { const url = `${this.apiUrl}/jobs`; const jobIds = products.map((product) => product.metadata.job.job_id); @@ -191,13 +191,27 @@ export class Hyp3ApiService { return from(jobIds).pipe( bufferCount(100), mergeMap((jobIdsBatch) => { - return this.http.patch( - url, - { name: newProjectName, job_ids: jobIdsBatch }, - { withCredentials: true }, - ); + return this.http + .patch( + url, + { name: newProjectName, job_ids: jobIdsBatch }, + { withCredentials: true }, + ) + .pipe( + map(() => ({ success: jobIdsBatch.length, failed: 0 })), + catchError(() => of({ success: 0, failed: jobIdsBatch.length })), + ); }, 5), toArray(), + map((results) => + results.reduce( + (acc, curr) => ({ + success: acc.success + curr.success, + failed: acc.failed + curr.failed, + }), + { success: 0, failed: 0 }, + ), + ), ); } diff --git a/src/app/services/notification.service.ts b/src/app/services/notification.service.ts index 919fcac48..44dd5fdc4 100644 --- a/src/app/services/notification.service.ts +++ b/src/app/services/notification.service.ts @@ -180,6 +180,17 @@ export class NotificationService { message: string, title = '', options: Partial = {}, + ): ActiveToast { + return this.toastr.error(message, title, { + ...options, + ...this.toastOptions, + }); + } + + public warn( + message: string, + title = '', + options: Partial = {}, ): ActiveToast { return this.toastr.warning(message, title, { ...options, diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json index b493c816a..3c75d521b 100644 --- a/src/assets/i18n/de.json +++ b/src/assets/i18n/de.json @@ -595,6 +595,10 @@ "RESTRICTED_DATA_USE_AGREEMENT": "Vereinbarung zur eingeschränkten Datennutzung", "RESUBMIT_JOB": "Erneutes Einreichen des Auftrags", "RESUBMIT_PROJECT": "Projekt erneut einreichen", + "RENAME_ALL_FAILED": "{{count}} Auftrag/Aufträge konnten nicht umbenannt werden.", + "RENAME_ERROR": "Beim Umbenennen der Aufträge ist ein Fehler aufgetreten.", + "RENAME_PARTIAL_SUCCESS": "{{success}} Auftrag/Aufträge umbenannt. {{failed}} fehlgeschlagen.", + "RENAME_SUCCESS": "{{count}} Auftrag/Aufträge erfolgreich umbenannt.", "RENAMING_JOBS": "Aufträge werden umbenannt...", "WILL_RENAME_JOBS": "{{count}} Auftrag/Aufträge werden umbenannt.", "RESULTS_TO_ON_DEMAND_QUEUE": "Ergebnisse in die On-Demand-Warteschlange", diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index a994143b3..52ea9f254 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -714,6 +714,10 @@ "RESTRICTED_DATA_USE_AGREEMENT": "Restricted Data Use Agreement", "RESUBMIT_JOB": "Resubmit Job", "RESUBMIT_PROJECT": "Resubmit Project", + "RENAME_ALL_FAILED": "Failed to rename {{count}} job(s).", + "RENAME_ERROR": "An error occurred while renaming jobs.", + "RENAME_PARTIAL_SUCCESS": "Renamed {{success}} job(s). {{failed}} failed.", + "RENAME_SUCCESS": "Successfully renamed {{count}} job(s).", "RENAMING_JOBS": "Renaming jobs...", "WILL_RENAME_JOBS": "This will rename {{count}} job(s).", "RESULTS_TO_ON_DEMAND_QUEUE": "results to On Demand queue", diff --git a/src/assets/i18n/es.json b/src/assets/i18n/es.json index 4603435fe..607d5a1d8 100644 --- a/src/assets/i18n/es.json +++ b/src/assets/i18n/es.json @@ -715,6 +715,10 @@ "RESTRICTED_DATA_USE_AGREEMENT": "Acuerdo de Uso de Datos Restringidos", "RESUBMIT_JOB": "Reenviar trabajo", "RESUBMIT_PROJECT": "Reenviar Proyecto", + "RENAME_ALL_FAILED": "Error al renombrar {{count}} trabajo(s).", + "RENAME_ERROR": "Ocurrió un error al renombrar los trabajos.", + "RENAME_PARTIAL_SUCCESS": "Se renombraron {{success}} trabajo(s). {{failed}} fallaron.", + "RENAME_SUCCESS": "Se renombraron {{count}} trabajo(s) exitosamente.", "RENAMING_JOBS": "Renombrando trabajos...", "WILL_RENAME_JOBS": "Se renombrarán {{count}} trabajo(s).", "RESULTS_TO_ON_DEMAND_QUEUE": "resultados a la lista On Demand", From a27b6d0919228e04e1b28b29d8242c50e0cf6789 Mon Sep 17 00:00:00 2001 From: Andrew Anderson Date: Wed, 17 Dec 2025 14:54:40 -0800 Subject: [PATCH 19/59] Update src/app/services/hyp3/hyp3-api.service.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/app/services/hyp3/hyp3-api.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/services/hyp3/hyp3-api.service.ts b/src/app/services/hyp3/hyp3-api.service.ts index 8b6e0ba4b..c3b175179 100644 --- a/src/app/services/hyp3/hyp3-api.service.ts +++ b/src/app/services/hyp3/hyp3-api.service.ts @@ -201,7 +201,7 @@ export class Hyp3ApiService { map(() => ({ success: jobIdsBatch.length, failed: 0 })), catchError(() => of({ success: 0, failed: jobIdsBatch.length })), ); - }, 5), + }, 3), toArray(), map((results) => results.reduce( From 00e98cc14578d56be59f8a5deaf4bdeb3924c6f4 Mon Sep 17 00:00:00 2001 From: Andrew Anderson Date: Wed, 17 Dec 2025 14:56:00 -0800 Subject: [PATCH 20/59] Update src/app/store/scenes/scenes.action.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/app/store/scenes/scenes.action.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/store/scenes/scenes.action.ts b/src/app/store/scenes/scenes.action.ts index 13a224d8b..820a5065e 100644 --- a/src/app/store/scenes/scenes.action.ts +++ b/src/app/store/scenes/scenes.action.ts @@ -30,7 +30,7 @@ export enum ScenesActionType { CLOSE_ZIP_CONTENTS = '[Scenes] Close Zip Contents', ADD_CMR_DATA_TO_ON_DEMAND_JOBS = '[Scenes] Add CMR Data to On Demand Jobs', - UPDATE_PRODUCT_WITH_NEW_PROJECT_NAME = '[Scenes] Update proudct with new project name', + UPDATE_PRODUCT_WITH_NEW_PROJECT_NAME = '[Scenes] Update product with new project name', SET_SELECTED_SCENE = '[Scenes] Set Selected Scene', SET_SELECTED_PAIR = '[Scenes] Set Selected Pair', From 5c4bae8252cfb2e2827cc1071e3520dcdcde0818 Mon Sep 17 00:00:00 2001 From: Andrew Anderson Date: Wed, 17 Dec 2025 16:28:46 -0800 Subject: [PATCH 21/59] Fix for flight direction graphics not appearing in Displacement I think this is an unreported bug resulting from the recent Angular upgrade. --- .../timeseries-header/timeseries-header.component.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/components/header/timeseries-header/timeseries-header.component.html b/src/app/components/header/timeseries-header/timeseries-header.component.html index 88b167654..49bdff026 100644 --- a/src/app/components/header/timeseries-header/timeseries-header.component.html +++ b/src/app/components/header/timeseries-header/timeseries-header.component.html @@ -16,7 +16,7 @@ class="invert-icon" alt="Azimuth" height="65" - width="auto" + width="65" /> } @if (flightDesc) { @@ -25,7 +25,7 @@ class="invert-icon" alt="Azimuth" height="65" - width="auto" + width="65" /> } @@ -45,7 +45,7 @@ [class.flip-icon]="flightDesc" alt="Line of Site" height="70" - width="auto" + width="84" /> From a72f7b772564d83bf34f5e84de88fcf27bc48912 Mon Sep 17 00:00:00 2001 From: William Horn Date: Wed, 17 Dec 2025 16:26:45 -0900 Subject: [PATCH 22/59] Show projects that are going to be renamed with the bulk operation. --- .../scenes-list-header.component.ts | 94 +++++++++---------- .../project-name-dialog.component.html | 25 ++++- .../project-name-dialog.component.ts | 17 +++- .../services/project-name-dialog.service.ts | 8 +- 4 files changed, 89 insertions(+), 55 deletions(-) diff --git a/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.ts b/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.ts index a626a4475..76e7781a4 100644 --- a/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.ts +++ b/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.ts @@ -685,56 +685,54 @@ export class ScenesListHeaderComponent implements OnInit, OnDestroy { } public onBulkProjectNameUpdate(): void { - this.projectNameDialog - .open('', this.products.length) - .subscribe((result) => { - if (result !== undefined) { - this.snackBar.open( - this.language.translate.instant('RENAMING_JOBS'), - '', - { - horizontalPosition: 'center', - verticalPosition: 'bottom', - }, - ); - - this.hyp3.updateJobsName$(this.products, result).subscribe({ - next: ({ success, failed }) => { - this.snackBar.dismiss(); - - if (failed === 0) { - this.notificationService.info( - this.language.translate.instant('RENAME_SUCCESS', { - count: success, - }), - ); - } else if (success === 0) { - this.notificationService.error( - this.language.translate.instant('RENAME_ALL_FAILED', { - count: failed, - }), - ); - } else { - this.notificationService.warn( - this.language.translate.instant('RENAME_PARTIAL_SUCCESS', { - success, - failed, - }), - ); - } - - this.store$.dispatch(new scenesStore.ClearScenes()); - this.store$.dispatch(new searchStore.MakeSearch()); - }, - error: () => { - this.snackBar.dismiss(); + this.projectNameDialog.open('', this.products).subscribe((result) => { + if (result !== undefined) { + this.snackBar.open( + this.language.translate.instant('RENAMING_JOBS'), + '', + { + horizontalPosition: 'center', + verticalPosition: 'bottom', + }, + ); + + this.hyp3.updateJobsName$(this.products, result).subscribe({ + next: ({ success, failed }) => { + this.snackBar.dismiss(); + + if (failed === 0) { + this.notificationService.info( + this.language.translate.instant('RENAME_SUCCESS', { + count: success, + }), + ); + } else if (success === 0) { this.notificationService.error( - this.language.translate.instant('RENAME_ERROR'), + this.language.translate.instant('RENAME_ALL_FAILED', { + count: failed, + }), ); - }, - }); - } - }); + } else { + this.notificationService.warn( + this.language.translate.instant('RENAME_PARTIAL_SUCCESS', { + success, + failed, + }), + ); + } + + this.store$.dispatch(new filtersStore.SetProjectName(result)); + this.store$.dispatch(new searchStore.MakeSearch()); + }, + error: () => { + this.snackBar.dismiss(); + this.notificationService.error( + this.language.translate.instant('RENAME_ERROR'), + ); + }, + }); + } + }); } ngOnDestroy(): void { diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.html b/src/app/components/shared/project-name-dialog/project-name-dialog.component.html index a60dbcf03..a805e9101 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.html +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.html @@ -1,20 +1,37 @@

{{ - (data.jobCount > 1 ? 'EDIT_ALL_PROJECT_NAMES' : 'EDIT_PROJECT_NAME') + (jobCount > 1 ? 'EDIT_ALL_PROJECT_NAMES' : 'EDIT_PROJECT_NAME') | translate }}

- @if (data.jobCount) { + @if (jobCount) {

- {{ 'WILL_RENAME_JOBS' | translate: { count: data.jobCount } }} + {{ 'WILL_RENAME_JOBS' | translate: { count: jobCount } }}

} + + @if (uniqueProjectNames) { +

+ {{ 'WILL_RENAME_PROJECTS' | translate }} +

+ + + @for (name of uniqueProjectNames; track name) { + + {{ name }} + + } + + } {{ 'PROJECT_NAME' | translate }} (MAT_DIALOG_DATA); public projectName: string; + public jobCount = null; + public uniqueProjectNames: Set | null = null; constructor() { this.projectName = this.data.currentName; + + const products = this.data?.products; + this.jobCount = this.data?.products?.length; + + this.uniqueProjectNames = new Set( + products + .filter((product) => product.metadata?.job.name) + .map((product) => product.metadata?.job.name), + ); } public get isValid(): boolean { diff --git a/src/app/services/project-name-dialog.service.ts b/src/app/services/project-name-dialog.service.ts index 17420e0c7..1de200dae 100644 --- a/src/app/services/project-name-dialog.service.ts +++ b/src/app/services/project-name-dialog.service.ts @@ -6,19 +6,23 @@ import { ProjectNameDialogComponent, ProjectNameDialogData, } from '@components/shared/project-name-dialog'; +import { CMRProduct } from '@models'; @Injectable({ providedIn: 'root' }) export class ProjectNameDialogService { private dialog = inject(MatDialog); - open(currentName: string, jobCount?: number): Observable { + open( + currentName: string, + products?: CMRProduct[], + ): Observable { const dialogRef = this.dialog.open< ProjectNameDialogComponent, ProjectNameDialogData, string >(ProjectNameDialogComponent, { width: '400px', - data: { currentName, jobCount }, + data: { currentName, products }, }); return dialogRef.afterClosed(); From 897fd7bd945b44c8ce59cfa0176d46e1d42e0d40 Mon Sep 17 00:00:00 2001 From: William Horn Date: Wed, 17 Dec 2025 16:43:12 -0900 Subject: [PATCH 23/59] Update uniqueProjectNames filtering --- .../project-name-dialog/project-name-dialog.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts index ba487a36c..ba26e6dc9 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts @@ -53,8 +53,8 @@ export class ProjectNameDialogComponent { this.uniqueProjectNames = new Set( products - .filter((product) => product.metadata?.job.name) - .map((product) => product.metadata?.job.name), + .map((product) => product.metadata?.job.name) + .filter((projectName) => !!projectName), ); } From 187df3c10d93a6ef88e9b588268618264408b462 Mon Sep 17 00:00:00 2001 From: William Horn Date: Wed, 17 Dec 2025 16:44:58 -0900 Subject: [PATCH 24/59] Fix: update job count in dialog --- .../shared/project-name-dialog/project-name-dialog.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts index ba26e6dc9..0ee827298 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts @@ -49,7 +49,7 @@ export class ProjectNameDialogComponent { this.projectName = this.data.currentName; const products = this.data?.products; - this.jobCount = this.data?.products?.length; + this.jobCount = products?.length; this.uniqueProjectNames = new Set( products From 55d62ddc372f3f493becd597391bae2f3dd70dfd Mon Sep 17 00:00:00 2001 From: William Horn Date: Thu, 18 Dec 2025 09:40:08 -0900 Subject: [PATCH 25/59] Fix single job rename --- .../project-name-dialog.component.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts index 0ee827298..d725fe977 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts @@ -51,11 +51,13 @@ export class ProjectNameDialogComponent { const products = this.data?.products; this.jobCount = products?.length; - this.uniqueProjectNames = new Set( - products - .map((product) => product.metadata?.job.name) - .filter((projectName) => !!projectName), - ); + if (products) { + this.uniqueProjectNames = new Set( + products + .map((product) => product.metadata?.job.name) + .filter((projectName) => !!projectName), + ); + } } public get isValid(): boolean { From f882f871a27e0b4cf343729743e1d8bb16f5ffb5 Mon Sep 17 00:00:00 2001 From: Andrew Anderson Date: Thu, 18 Dec 2025 12:59:02 -0800 Subject: [PATCH 26/59] Many Changes * Added list of projects and associated job counts in a sortable table * Limited project name input to 100 characters * Prevent user from changing names of projects for other users --- .../project-name-dialog.component.html | 98 ++++++++++++------ .../project-name-dialog.component.scss | 99 ++++++++++++++++++- .../project-name-dialog.component.ts | 64 ++++++++++-- .../services/project-name-dialog.service.ts | 37 +++++-- src/assets/i18n/de.json | 2 + src/assets/i18n/en.json | 3 + src/assets/i18n/es.json | 3 + 7 files changed, 252 insertions(+), 54 deletions(-) diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.html b/src/app/components/shared/project-name-dialog/project-name-dialog.component.html index a805e9101..5c6197f9c 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.html +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.html @@ -1,37 +1,26 @@ -

- {{ - (jobCount > 1 ? 'EDIT_ALL_PROJECT_NAMES' : 'EDIT_PROJECT_NAME') - | translate - }} -

+
+

+ {{ + (jobCount > 1 ? 'EDIT_ALL_PROJECT_NAMES' : 'EDIT_PROJECT_NAME') + | translate + }} +

+ @if (isDisabledByUserFilter) { +

+ {{ 'CANNOT_RENAME_PROJECTS_FOR' | translate }}: {{ filterUserId }} +

+ } @else if (jobCount) { +

+ {{ 'WILL_RENAME_JOBS' | translate: { count: jobCount } }} +

+ } +
- @if (jobCount) { -

- {{ 'WILL_RENAME_JOBS' | translate: { count: jobCount } }} -

- } - - @if (uniqueProjectNames) { -

- {{ 'WILL_RENAME_PROJECTS' | translate }} -

- - - @for (name of uniqueProjectNames; track name) { - - {{ name }} - - } - - } {{ 'PROJECT_NAME' | translate }} [(ngModel)]="projectName" name="projectName" cdkFocusInitial + maxlength="100" + [disabled]="isDisabledByUserFilter" /> + {{ projectName?.length || 0 }}/100 + @if (projectName?.length >= 100) { + {{ 'PROJECT_NAME_MAX_LENGTH' | translate }} + } + + @if (projectNameCounts && projectNameCounts.size > 0) { +

+ {{ 'PROJECTS_TO_RENAME' | translate }} +

+
+ + + + + + + +
+ {{ 'PROJECT_NAME' | translate }} + @if (sortColumn === 'name') { + {{ sortDirection === 'asc' ? '▲' : '▼' }} + } + + {{ 'JOBS' | translate }} + @if (sortColumn === 'count') { + {{ sortDirection === 'asc' ? '▲' : '▼' }} + } +
+
+ + + @for (entry of sortedEntries; track entry.key) { + + + + + } + +
{{ entry.key }}{{ entry.value }}
+
+
+ }
diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss b/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss index d092d6ca7..60a05ec64 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss @@ -1,15 +1,104 @@ -.project-name-field { - width: 100%; +::ng-deep .mat-mdc-dialog-title { + padding-top: 24px !important; + + &::before { + display: none; + } +} + +.dialog-title { + margin: 0; } -.job-count-info { - margin: 0 0 16px 0; +.dialog-subtitle { + margin: 4px 0 0 0; opacity: 0.7; font-size: 14px; - &.job-count-warning { + &.job-count-warning, + &.cannot-rename-warning { color: #f44336; opacity: 1; font-weight: 500; } } + +.project-name-field { + width: 100%; + + ::ng-deep .mat-mdc-text-field-wrapper { + padding-top: 0; + } +} + +.projects-label { + margin: 16px 0 8px 0; + opacity: 0.7; + font-size: 14px; +} + +.projects-table-wrapper { + border: 1px solid rgba(0, 0, 0, 0.12); + border-radius: 4px; + margin-bottom: 8px; + + &.scrollable { + .projects-table-body-container { + max-height: 185px; + overflow-y: scroll; + } + + > .projects-table .job-count-header { + padding-right: 29px; // 12px base + 17px scrollbar width + } + } +} + +.projects-table-body-container { + border-top: 1px solid rgba(0, 0, 0, 0.12); +} + +.projects-table { + width: 100%; + border-collapse: collapse; + + th, + td { + padding: 8px 12px; + font-size: 14px; + text-align: left; + } + + th { + font-weight: 500; + opacity: 0.7; + + &.sortable { + cursor: pointer; + user-select: none; + + &:hover { + opacity: 1; + } + } + } + + td { + border-top: 1px solid rgba(0, 0, 0, 0.12); + } + + tr:first-child td { + border-top: none; + } + + .sort-indicator { + font-size: 10px; + margin-left: 4px; + } + + .job-count-header, + .job-count-cell { + text-align: right; + width: 80px; + } +} diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts index d725fe977..ee7b8fc86 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts @@ -13,11 +13,15 @@ import { MatButtonModule } from '@angular/material/button'; import { TranslateModule } from '@ngx-translate/core'; import * as models from '@models'; -import { MatChipsModule } from '@angular/material/chips'; + +type SortColumn = 'name' | 'count'; +type SortDirection = 'asc' | 'desc'; export interface ProjectNameDialogData { currentName: string; products?: models.CMRProduct[]; + loggedInUserId?: string; + filterUserId?: string; } @Component({ @@ -34,7 +38,6 @@ export interface ProjectNameDialogData { MatInputModule, MatButtonModule, TranslateModule, - MatChipsModule, ], }) export class ProjectNameDialogComponent { @@ -43,21 +46,66 @@ export class ProjectNameDialogComponent { public projectName: string; public jobCount = null; - public uniqueProjectNames: Set | null = null; + public projectNameCounts: Map | null = null; + public sortedEntries: Array<{ key: string; value: number }> = []; + public sortColumn: SortColumn = 'name'; + public sortDirection: SortDirection = 'asc'; + public isDisabledByUserFilter = false; + public filterUserId: string; constructor() { this.projectName = this.data.currentName; + // Disable input if filtering by a different user's jobs + const { loggedInUserId, filterUserId } = this.data; + this.filterUserId = filterUserId; + if (filterUserId && loggedInUserId && filterUserId !== loggedInUserId) { + this.isDisabledByUserFilter = true; + } + const products = this.data?.products; this.jobCount = products?.length; if (products) { - this.uniqueProjectNames = new Set( - products - .map((product) => product.metadata?.job.name) - .filter((projectName) => !!projectName), - ); + const counts = new Map(); + products.forEach((product) => { + const name = product.metadata?.job?.name; + if (name) { + counts.set(name, (counts.get(name) || 0) + 1); + } + }); + this.projectNameCounts = counts.size > 0 ? counts : null; + this.updateSortedEntries(); + } + } + + public sortBy(column: SortColumn): void { + if (this.sortColumn === column) { + this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc'; + } else { + this.sortColumn = column; + this.sortDirection = 'asc'; } + this.updateSortedEntries(); + } + + private updateSortedEntries(): void { + if (!this.projectNameCounts) { + this.sortedEntries = []; + return; + } + + this.sortedEntries = Array.from(this.projectNameCounts.entries()) + .map(([key, value]) => ({ key, value })) + .sort((a, b) => { + let comparison: number; + if (this.sortColumn === 'name') { + comparison = a.key.localeCompare(b.key); + } else { + comparison = a.value - b.value; + } + return this.sortDirection === 'asc' ? comparison : -comparison; + }); } public get isValid(): boolean { diff --git a/src/app/services/project-name-dialog.service.ts b/src/app/services/project-name-dialog.service.ts index 1de200dae..abc1f579b 100644 --- a/src/app/services/project-name-dialog.service.ts +++ b/src/app/services/project-name-dialog.service.ts @@ -1,30 +1,47 @@ import { Injectable, inject } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; -import { Observable } from 'rxjs'; +import { Store } from '@ngrx/store'; +import { Observable, take, combineLatest, switchMap } from 'rxjs'; import { ProjectNameDialogComponent, ProjectNameDialogData, } from '@components/shared/project-name-dialog'; import { CMRProduct } from '@models'; +import { AppState } from '@store'; +import * as hyp3Store from '@store/hyp3'; @Injectable({ providedIn: 'root' }) export class ProjectNameDialogService { private dialog = inject(MatDialog); + private store$ = inject>(Store); open( currentName: string, products?: CMRProduct[], ): Observable { - const dialogRef = this.dialog.open< - ProjectNameDialogComponent, - ProjectNameDialogData, - string - >(ProjectNameDialogComponent, { - width: '400px', - data: { currentName, products }, - }); + return combineLatest([ + this.store$.select(hyp3Store.getHyp3User), + this.store$.select(hyp3Store.getOnDemandUserId), + ]).pipe( + take(1), + switchMap(([hyp3User, filterUserId]) => { + const dialogRef = this.dialog.open< + ProjectNameDialogComponent, + ProjectNameDialogData, + string + >(ProjectNameDialogComponent, { + width: '400px', + data: { + currentName, + products, + loggedInUserId: hyp3User?.user_id, + filterUserId: filterUserId || undefined, + }, + }); - return dialogRef.afterClosed(); + return dialogRef.afterClosed(); + }), + ); } } diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json index 3c75d521b..17b01b348 100644 --- a/src/assets/i18n/de.json +++ b/src/assets/i18n/de.json @@ -104,6 +104,7 @@ "CAMPAIGN_NAME": "Name der Kampagne", "CAMPAIGN_SELECTOR": "Kampagnen-Selektor", "CANCEL": "Abbrechen", + "CANNOT_RENAME_PROJECTS_FOR": "Sie können Projekte nicht umbenennen für", "CENTER_COLUMN_AND_FILES_COLUMN_RIGHT_WILL_POPULATE": "(mittlere Spalte) und Dateispalte (rechts) werden ausgefüllt.", "CHART": "Diagramm", "CHEVRON_RIGHT": "chevron_right", @@ -561,6 +562,7 @@ "PROJECT_NAME": "Projektname", "PROJECT_NAME_UPDATE_ALL_TOOLTIP": "Projektname für alle geladenen Aufträge aktualisieren", "PROJECT_NAME_UPDATE_FOR_JOBS": "Projektname für {{count}} Aufträge aktualisieren", + "PROJECTS_TO_RENAME": "Umzubenennende Projekte:", "PUSH_PIN": "push_pin", "PYTHON_PY": "Python (.py)", "QUAKE": "beben", diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 52ea9f254..836759b99 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -123,6 +123,7 @@ "CAMPAIGN_SELECTOR": "Campaign Selector", "CANADAS_OPEN_LICENSE": "Canada's Open Government License", "CANCEL": "Cancel", + "CANNOT_RENAME_PROJECTS_FOR": "You cannot rename projects for", "CENTER_COLUMN_AND_FILES_COLUMN_RIGHT_WILL_POPULATE": "(center column) and Files column (right) will populate.", "CHARACTERS": "characters", "CHART": "Chart", @@ -670,8 +671,10 @@ "PROFILE": "Profile", "PROJECT": "Project", "PROJECT_NAME": "Project Name", + "PROJECT_NAME_MAX_LENGTH": "Project name cannot exceed 100 characters", "PROJECT_NAME_UPDATE_ALL_TOOLTIP": "Update project name for all loaded jobs", "PROJECT_NAME_UPDATE_FOR_JOBS": "Update project name for {{count}} jobs", + "PROJECTS_TO_RENAME": "Projects to rename:", "PUSH_PIN": "push_pin", "PYTHON_PY": "python (.py)", "QUAKE": "quake", diff --git a/src/assets/i18n/es.json b/src/assets/i18n/es.json index 607d5a1d8..bc204d4cf 100644 --- a/src/assets/i18n/es.json +++ b/src/assets/i18n/es.json @@ -123,6 +123,7 @@ "CAMPAIGN_SELECTOR": "Selector de Campaña", "CANADAS_OPEN_LICENSE": "Licencia de Gobierno Abierto de Canadá", "CANCEL": "Cancelar", + "CANNOT_RENAME_PROJECTS_FOR": "No puede cambiar el nombre de los proyectos de", "CENTER_COLUMN_AND_FILES_COLUMN_RIGHT_WILL_POPULATE": "(columna central) y la columna Archivos (derecha) se completarán.", "CHARACTERS": "caracteres", "CHART": "Gráfico", @@ -671,8 +672,10 @@ "PROFILE": "Perfil", "PROJECT": "Proyecto", "PROJECT_NAME": "Nombre del Proyecto", + "PROJECT_NAME_MAX_LENGTH": "El nombre del proyecto no puede exceder 100 caracteres", "PROJECT_NAME_UPDATE_ALL_TOOLTIP": "Actualizar nombre del proyecto para todos los trabajos cargados", "PROJECT_NAME_UPDATE_FOR_JOBS": "Actualizar nombre del proyecto para {{count}} trabajos", + "PROJECTS_TO_RENAME": "Proyectos a renombrar:", "PUSH_PIN": "chincheta", "PYTHON_PY": "python (.py)", "QUAKE": "terremoto", From 180c7e4a08de0d9b7281e2ddffcb47c5aba1563f Mon Sep 17 00:00:00 2001 From: Andrew Anderson Date: Thu, 18 Dec 2025 13:42:36 -0800 Subject: [PATCH 27/59] Progress Bar I've implemented a two-phase dialog that handles the rename operation internally with a determinate progress bar. --- .../scene-file/scene-file.component.ts | 2 +- .../scenes-list-header.component.ts | 54 +--- .../shared/project-name-dialog/index.ts | 1 + .../project-name-dialog.component.html | 234 ++++++++++++------ .../project-name-dialog.component.scss | 39 +++ .../project-name-dialog.component.ts | 82 +++++- src/app/services/hyp3/hyp3-api.service.ts | 64 ++++- .../services/project-name-dialog.service.ts | 14 +- src/assets/i18n/de.json | 2 + src/assets/i18n/en.json | 2 + src/assets/i18n/es.json | 2 + 11 files changed, 361 insertions(+), 135 deletions(-) diff --git a/src/app/components/results-menu/scene-files/scene-file/scene-file.component.ts b/src/app/components/results-menu/scene-files/scene-file/scene-file.component.ts index 7fed76a03..80964e364 100644 --- a/src/app/components/results-menu/scene-files/scene-file/scene-file.component.ts +++ b/src/app/components/results-menu/scene-files/scene-file/scene-file.component.ts @@ -214,7 +214,7 @@ export class SceneFileComponent implements OnInit, OnDestroy { public onEditProjectName(oldProjectName: string): void { this.projectNameDialog.open(oldProjectName).subscribe((result) => { - if (result !== undefined) { + if (result !== undefined && typeof result === 'string') { this.renameJobProjectName.emit(result); } }); diff --git a/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.ts b/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.ts index 76e7781a4..6b162c94f 100644 --- a/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.ts +++ b/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.ts @@ -33,7 +33,6 @@ import { NotificationService, ExportService, ProjectNameDialogService, - AsfLanguageService, } from '@services'; import * as models from '@models'; @@ -47,7 +46,6 @@ import { CodeExportType, } from '@components/shared/code-export'; import { MatDialog } from '@angular/material/dialog'; -import { MatSnackBar } from '@angular/material/snack-bar'; import { NgClass, AsyncPipe, @@ -107,9 +105,7 @@ export class ScenesListHeaderComponent implements OnInit, OnDestroy { private possibleHyp3JobsService = inject(PossibleHyp3JobsService); private exportService = inject(ExportService); private dialog = inject(MatDialog); - private snackBar = inject(MatSnackBar); private projectNameDialog = inject(ProjectNameDialogService); - private language = inject(AsfLanguageService); public copyIcon = faCopy; public pairs$ = this.pairService.pairs$; @@ -686,51 +682,11 @@ export class ScenesListHeaderComponent implements OnInit, OnDestroy { public onBulkProjectNameUpdate(): void { this.projectNameDialog.open('', this.products).subscribe((result) => { - if (result !== undefined) { - this.snackBar.open( - this.language.translate.instant('RENAMING_JOBS'), - '', - { - horizontalPosition: 'center', - verticalPosition: 'bottom', - }, - ); - - this.hyp3.updateJobsName$(this.products, result).subscribe({ - next: ({ success, failed }) => { - this.snackBar.dismiss(); - - if (failed === 0) { - this.notificationService.info( - this.language.translate.instant('RENAME_SUCCESS', { - count: success, - }), - ); - } else if (success === 0) { - this.notificationService.error( - this.language.translate.instant('RENAME_ALL_FAILED', { - count: failed, - }), - ); - } else { - this.notificationService.warn( - this.language.translate.instant('RENAME_PARTIAL_SUCCESS', { - success, - failed, - }), - ); - } - - this.store$.dispatch(new filtersStore.SetProjectName(result)); - this.store$.dispatch(new searchStore.MakeSearch()); - }, - error: () => { - this.snackBar.dismiss(); - this.notificationService.error( - this.language.translate.instant('RENAME_ERROR'), - ); - }, - }); + if (result !== undefined && typeof result !== 'string') { + // Dialog now handles the rename operation and shows progress + // We just need to update the filter and refresh search + this.store$.dispatch(new filtersStore.SetProjectName(result.newName)); + this.store$.dispatch(new searchStore.MakeSearch()); } }); } diff --git a/src/app/components/shared/project-name-dialog/index.ts b/src/app/components/shared/project-name-dialog/index.ts index 34c9f1049..c6526b119 100644 --- a/src/app/components/shared/project-name-dialog/index.ts +++ b/src/app/components/shared/project-name-dialog/index.ts @@ -1,4 +1,5 @@ export { ProjectNameDialogComponent, ProjectNameDialogData, + ProjectNameDialogResult, } from './project-name-dialog.component'; diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.html b/src/app/components/shared/project-name-dialog/project-name-dialog.component.html index 5c6197f9c..9e88e2c97 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.html +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.html @@ -1,90 +1,166 @@ -
-

- {{ - (jobCount > 1 ? 'EDIT_ALL_PROJECT_NAMES' : 'EDIT_PROJECT_NAME') - | translate - }} -

- @if (isDisabledByUserFilter) { -

- {{ 'CANNOT_RENAME_PROJECTS_FOR' | translate }}: {{ filterUserId }} -

- } @else if (jobCount) { -

- {{ 'WILL_RENAME_JOBS' | translate: { count: jobCount } }} -

- } -
- - - - - {{ 'PROJECT_NAME' | translate }} - - {{ projectName?.length || 0 }}/100 - @if (projectName?.length >= 100) { - {{ 'PROJECT_NAME_MAX_LENGTH' | translate }} - } - - - @if (projectNameCounts && projectNameCounts.size > 0) { -

- {{ 'PROJECTS_TO_RENAME' | translate }} + +@if (phase === 'input') { +

+

+ {{ + (jobCount > 1 ? 'EDIT_ALL_PROJECT_NAMES' : 'EDIT_PROJECT_NAME') + | translate + }} +

+ @if (isDisabledByUserFilter) { +

+ {{ 'CANNOT_RENAME_PROJECTS_FOR' | translate }}: {{ filterUserId }}

-
- - - - - - - -
- {{ 'PROJECT_NAME' | translate }} - @if (sortColumn === 'name') { - {{ sortDirection === 'asc' ? '▲' : '▼' }} - } - - {{ 'JOBS' | translate }} - @if (sortColumn === 'count') { - {{ sortDirection === 'asc' ? '▲' : '▼' }} - } -
-
+ {{ 'WILL_RENAME_JOBS' | translate: { count: jobCount } }} +

+ } +
+ + + + + {{ 'PROJECT_NAME' | translate }} + + {{ projectName?.length || 0 }}/100 + @if (projectName?.length >= 100) { + {{ 'PROJECT_NAME_MAX_LENGTH' | translate }} + } + + + @if (projectNameCounts && projectNameCounts.size > 0) { +

+ {{ 'PROJECTS_TO_RENAME' | translate }} +

+
- - @for (entry of sortedEntries; track entry.key) { - - - - - } - + + + + + +
{{ entry.key }}{{ entry.value }}
+ {{ 'PROJECT_NAME' | translate }} + @if (sortColumn === 'name') { + {{ sortDirection === 'asc' ? '▲' : '▼' }} + } + + {{ 'JOBS' | translate }} + @if (sortColumn === 'count') { + {{ sortDirection === 'asc' ? '▲' : '▼' }} + } +
+
+ + + @for (entry of sortedEntries; track entry.key) { + + + + + } + +
{{ entry.key }}{{ entry.value }}
+
-
- } + } + + + + + + + +} + + +@if (phase === 'processing') { +
+

{{ 'RENAMING_JOBS' | translate }}

+
+ + +
+ +

{{ progress }}%

+
- - - +} diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss b/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss index 60a05ec64..11e5542c9 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss @@ -102,3 +102,42 @@ width: 80px; } } + +// Progress bar styles +.progress-container { + text-align: center; + padding: 24px 0; + + mat-progress-bar { + margin-bottom: 16px; + } + + .progress-text { + margin: 0; + font-size: 18px; + font-weight: 500; + } +} + +// Result styles +.result-container { + text-align: center; + padding: 16px 0; + + p { + margin: 0; + font-size: 14px; + } + + .result-success { + color: #4caf50; + } + + .result-warning { + color: #ff9800; + } + + .result-error { + color: #f44336; + } +} diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts index ee7b8fc86..f0ef7b3a5 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts @@ -10,13 +10,18 @@ import { import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatButtonModule } from '@angular/material/button'; +import { MatProgressBarModule } from '@angular/material/progress-bar'; import { TranslateModule } from '@ngx-translate/core'; +import { Subscription } from 'rxjs'; import * as models from '@models'; +import { Hyp3ApiService } from '@services'; type SortColumn = 'name' | 'count'; type SortDirection = 'asc' | 'desc'; +export type DialogPhase = 'input' | 'processing' | 'complete' | 'error'; + export interface ProjectNameDialogData { currentName: string; products?: models.CMRProduct[]; @@ -24,6 +29,12 @@ export interface ProjectNameDialogData { filterUserId?: string; } +export interface ProjectNameDialogResult { + newName: string; + success: number; + failed: number; +} + @Component({ selector: 'app-project-name-dialog', templateUrl: './project-name-dialog.component.html', @@ -37,12 +48,14 @@ export interface ProjectNameDialogData { MatFormFieldModule, MatInputModule, MatButtonModule, + MatProgressBarModule, TranslateModule, ], }) export class ProjectNameDialogComponent { dialogRef = inject>(MatDialogRef); data = inject(MAT_DIALOG_DATA); + private hyp3Api = inject(Hyp3ApiService); public projectName: string; public jobCount = null; @@ -53,6 +66,14 @@ export class ProjectNameDialogComponent { public isDisabledByUserFilter = false; public filterUserId: string; + // Two-phase UI state + public phase: DialogPhase = 'input'; + public progress = 0; + public successCount = 0; + public failedCount = 0; + + private subscriptions: Subscription[] = []; + constructor() { this.projectName = this.data.currentName; @@ -77,6 +98,9 @@ export class ProjectNameDialogComponent { this.projectNameCounts = counts.size > 0 ? counts : null; this.updateSortedEntries(); } + + // Prevent closing during processing + this.dialogRef.disableClose = false; } public sortBy(column: SortColumn): void { @@ -117,8 +141,62 @@ export class ProjectNameDialogComponent { } public onSave(): void { - if (this.isValid) { - this.dialogRef.close(this.projectName.trim()); + if (!this.isValid) { + return; } + + const trimmedName = this.projectName.trim(); + + // If no products, just return the name (single-file rename flow) + if (!this.data.products || this.data.products.length === 0) { + this.dialogRef.close(trimmedName); + return; + } + + // Transition to processing phase for bulk rename + this.phase = 'processing'; + this.dialogRef.disableClose = true; + this.progress = 0; + + const { progress$, result$ } = this.hyp3Api.updateJobsNameWithProgress$( + this.data.products, + trimmedName, + ); + + // Subscribe to progress updates + this.subscriptions.push( + progress$.subscribe((progress) => { + this.progress = progress; + }), + ); + + // Subscribe to final result + this.subscriptions.push( + result$.subscribe({ + next: ({ success, failed }) => { + this.successCount = success; + this.failedCount = failed; + this.phase = 'complete'; + this.dialogRef.disableClose = false; + }, + error: () => { + this.phase = 'error'; + this.dialogRef.disableClose = false; + }, + }), + ); + } + + public onDone(): void { + const result: ProjectNameDialogResult = { + newName: this.projectName.trim(), + success: this.successCount, + failed: this.failedCount, + }; + this.dialogRef.close(result); + } + + ngOnDestroy(): void { + this.subscriptions.forEach((sub) => sub.unsubscribe()); } } diff --git a/src/app/services/hyp3/hyp3-api.service.ts b/src/app/services/hyp3/hyp3-api.service.ts index c3b175179..dab54c11d 100644 --- a/src/app/services/hyp3/hyp3-api.service.ts +++ b/src/app/services/hyp3/hyp3-api.service.ts @@ -5,8 +5,8 @@ import { HttpParams, } from '@angular/common/http'; -import { Observable, of, first, catchError, map, forkJoin, from } from 'rxjs'; -import { mergeMap, toArray, bufferCount } from 'rxjs/operators'; +import { Observable, of, first, catchError, map, forkJoin, from, Subject } from 'rxjs'; +import { mergeMap, toArray, bufferCount, tap, finalize } from 'rxjs/operators'; import * as moment from 'moment'; import * as models from '@models'; @@ -215,6 +215,66 @@ export class Hyp3ApiService { ); } + /** + * Updates job names with progress reporting. + * Returns an object with: + * - progress$: Observable that emits progress percentage (0-100) as batches complete + * - result$: Observable that emits the final result when all batches are done + */ + public updateJobsNameWithProgress$( + products: models.CMRProduct[], + newProjectName: string, + ): { + progress$: Observable; + result$: Observable<{ success: number; failed: number }>; + } { + const url = `${this.apiUrl}/jobs`; + const jobIds = products.map((product) => product.metadata.job.job_id); + const totalJobs = jobIds.length; + const batchSize = 100; + const totalBatches = Math.ceil(totalJobs / batchSize); + + if (!newProjectName) { + newProjectName = null; + } + + const progressSubject = new Subject(); + let completedBatches = 0; + let successCount = 0; + let failedCount = 0; + + const result$ = from(jobIds).pipe( + bufferCount(batchSize), + mergeMap((jobIdsBatch) => { + return this.http + .patch( + url, + { name: newProjectName, job_ids: jobIdsBatch }, + { withCredentials: true }, + ) + .pipe( + map(() => ({ success: jobIdsBatch.length, failed: 0 })), + catchError(() => of({ success: 0, failed: jobIdsBatch.length })), + tap((batchResult) => { + completedBatches++; + successCount += batchResult.success; + failedCount += batchResult.failed; + const progress = Math.round((completedBatches / totalBatches) * 100); + progressSubject.next(progress); + }), + ); + }, 3), + toArray(), + map(() => ({ success: successCount, failed: failedCount })), + finalize(() => progressSubject.complete()), + ); + + return { + progress$: progressSubject.asObservable(), + result$, + }; + } + public submitJobBatch$(jobBatch: object) { const submitJobUrl = `${this.apiUrl}/jobs`; diff --git a/src/app/services/project-name-dialog.service.ts b/src/app/services/project-name-dialog.service.ts index abc1f579b..b9b1e5861 100644 --- a/src/app/services/project-name-dialog.service.ts +++ b/src/app/services/project-name-dialog.service.ts @@ -6,6 +6,7 @@ import { Observable, take, combineLatest, switchMap } from 'rxjs'; import { ProjectNameDialogComponent, ProjectNameDialogData, + ProjectNameDialogResult, } from '@components/shared/project-name-dialog'; import { CMRProduct } from '@models'; import { AppState } from '@store'; @@ -16,10 +17,19 @@ export class ProjectNameDialogService { private dialog = inject(MatDialog); private store$ = inject>(Store); + /** + * Opens the project name dialog. + * + * When products are provided (bulk rename), the dialog handles the rename + * operation internally and returns ProjectNameDialogResult. + * + * When no products are provided (single-file rename), the dialog just + * returns the new name as a string. + */ open( currentName: string, products?: CMRProduct[], - ): Observable { + ): Observable { return combineLatest([ this.store$.select(hyp3Store.getHyp3User), this.store$.select(hyp3Store.getOnDemandUserId), @@ -29,7 +39,7 @@ export class ProjectNameDialogService { const dialogRef = this.dialog.open< ProjectNameDialogComponent, ProjectNameDialogData, - string + string | ProjectNameDialogResult >(ProjectNameDialogComponent, { width: '400px', data: { diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json index 17b01b348..e6e877a93 100644 --- a/src/assets/i18n/de.json +++ b/src/assets/i18n/de.json @@ -239,6 +239,7 @@ "ENTER_A_NAME_FOR_YOUR_SEARCH_IN_THE_SAVE_SEARCH_DIALOG": "Geben Sie im Dialogfeld \"Suche speichern\" einen Namen für Ihre Suche ein.", "ENVIRONMENT_FILE": "Umgebungsdatei", "EQUATORIAL_MAP_VIEW": "Äquatoriale Kartenansicht", + "ERROR": "Fehler", "ERS_DESC": "Hauptsächlich SAR-Bilder innerhalb der ASF und der McMurdo-Stationsmasken in der Antarktis. Die Daten sind für die Interferometrie geeignet.", "EVENT": "Ereignis", "EVENT_DATE": "Veranstaltungstermin", @@ -598,6 +599,7 @@ "RESUBMIT_JOB": "Erneutes Einreichen des Auftrags", "RESUBMIT_PROJECT": "Projekt erneut einreichen", "RENAME_ALL_FAILED": "{{count}} Auftrag/Aufträge konnten nicht umbenannt werden.", + "RENAME_COMPLETE": "Umbenennung abgeschlossen", "RENAME_ERROR": "Beim Umbenennen der Aufträge ist ein Fehler aufgetreten.", "RENAME_PARTIAL_SUCCESS": "{{success}} Auftrag/Aufträge umbenannt. {{failed}} fehlgeschlagen.", "RENAME_SUCCESS": "{{count}} Auftrag/Aufträge erfolgreich umbenannt.", diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 836759b99..4ea8a3fc9 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -292,6 +292,7 @@ "ENTER_A_NAME_FOR_YOUR_SEARCH_IN_THE_SAVE_SEARCH_DIALOG": "Enter a name for your search in the Save Search dialog.", "ENVIRONMENT_FILE": "Environment File", "EQUATORIAL_MAP_VIEW": "Equatorial Map View", + "ERROR": "Error", "ERS_DESC": "Primarily SAR imagery within the ASF and the McMurdo, Antarctica, station masks. Data is suitable for interferometry.", "EVENT": "Event", "EVENT_DATE": "Event Date", @@ -718,6 +719,7 @@ "RESUBMIT_JOB": "Resubmit Job", "RESUBMIT_PROJECT": "Resubmit Project", "RENAME_ALL_FAILED": "Failed to rename {{count}} job(s).", + "RENAME_COMPLETE": "Rename Complete", "RENAME_ERROR": "An error occurred while renaming jobs.", "RENAME_PARTIAL_SUCCESS": "Renamed {{success}} job(s). {{failed}} failed.", "RENAME_SUCCESS": "Successfully renamed {{count}} job(s).", diff --git a/src/assets/i18n/es.json b/src/assets/i18n/es.json index bc204d4cf..05b1e19f0 100644 --- a/src/assets/i18n/es.json +++ b/src/assets/i18n/es.json @@ -292,6 +292,7 @@ "ENTER_A_NAME_FOR_YOUR_SEARCH_IN_THE_SAVE_SEARCH_DIALOG": "Ingrese un nombre para su búsqueda en el cuadro de diálogo Guardar búsqueda.", "ENVIRONMENT_FILE": "Archivo de entorno", "EQUATORIAL_MAP_VIEW": "Ver Mapa Ecuatorial", + "ERROR": "Error", "ERS_DESC": "Principalmente imágenes SAR dentro de las máscaras de la estación ASF y McMurdo, Antártida. Los datos son adecuados para la interferometría.", "EVENT": "Evento", "EVENT_DATE": "Fecha del evento", @@ -719,6 +720,7 @@ "RESUBMIT_JOB": "Reenviar trabajo", "RESUBMIT_PROJECT": "Reenviar Proyecto", "RENAME_ALL_FAILED": "Error al renombrar {{count}} trabajo(s).", + "RENAME_COMPLETE": "Cambio de nombre completado", "RENAME_ERROR": "Ocurrió un error al renombrar los trabajos.", "RENAME_PARTIAL_SUCCESS": "Se renombraron {{success}} trabajo(s). {{failed}} fallaron.", "RENAME_SUCCESS": "Se renombraron {{count}} trabajo(s) exitosamente.", From c0436cdbb241ed4f53f2641d54ed4d67b37e49fe Mon Sep 17 00:00:00 2001 From: Andrew Anderson Date: Thu, 18 Dec 2025 14:43:46 -0800 Subject: [PATCH 28/59] Deterministic Progress Bar The Project Rename dialog box now is augmented with a couple more screens indicating renaming progress and completion. --- .../scenes-list-header.component.ts | 1 + .../project-name-dialog.component.html | 19 ++++++++++------- .../project-name-dialog.component.scss | 21 +++++++++++++++++++ .../project-name-dialog.component.ts | 5 +++++ src/assets/i18n/de.json | 2 ++ src/assets/i18n/en.json | 2 ++ src/assets/i18n/es.json | 2 ++ 7 files changed, 44 insertions(+), 8 deletions(-) diff --git a/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.ts b/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.ts index 6b162c94f..473e5ac4b 100644 --- a/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.ts +++ b/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.ts @@ -685,6 +685,7 @@ export class ScenesListHeaderComponent implements OnInit, OnDestroy { if (result !== undefined && typeof result !== 'string') { // Dialog now handles the rename operation and shows progress // We just need to update the filter and refresh search + this.store$.dispatch(new searchStore.ClearSearch()); this.store$.dispatch(new filtersStore.SetProjectName(result.newName)); this.store$.dispatch(new searchStore.MakeSearch()); } diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.html b/src/app/components/shared/project-name-dialog/project-name-dialog.component.html index 9e88e2c97..e902c755b 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.html +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.html @@ -11,13 +11,6 @@

{{ 'CANNOT_RENAME_PROJECTS_FOR' | translate }}: {{ filterUserId }}

- } @else if (jobCount) { -

- {{ 'WILL_RENAME_JOBS' | translate: { count: jobCount } }} -

}

@@ -78,6 +71,16 @@

+ + @if (!isDisabledByUserFilter) { + + {{ (projectCount === 1 ? 'CONFIRM_RENAME_SINGLE' : 'CONFIRM_RENAME_MULTIPLE') | translate: { projectCount: projectCount, jobCount: jobCount } }} + + } } @@ -85,7 +88,7 @@

- diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss b/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss index 11e5542c9..275f369c2 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss @@ -103,6 +103,27 @@ } } +// Confirmation checkbox +.confirmation-checkbox { + display: block; + margin-top: 32px; + + ::ng-deep { + .mdc-form-field { + align-items: flex-start; + } + + .mdc-checkbox { + margin-top: -9px; + + .mdc-checkbox__ripple, + .mat-mdc-checkbox-ripple { + display: none; + } + } + } +} + // Progress bar styles .progress-container { text-align: center; diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts index f0ef7b3a5..38cc842f2 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts @@ -11,6 +11,7 @@ import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatButtonModule } from '@angular/material/button'; import { MatProgressBarModule } from '@angular/material/progress-bar'; +import { MatCheckboxModule } from '@angular/material/checkbox'; import { TranslateModule } from '@ngx-translate/core'; import { Subscription } from 'rxjs'; @@ -49,6 +50,7 @@ export interface ProjectNameDialogResult { MatInputModule, MatButtonModule, MatProgressBarModule, + MatCheckboxModule, TranslateModule, ], }) @@ -59,12 +61,14 @@ export class ProjectNameDialogComponent { public projectName: string; public jobCount = null; + public projectCount = 0; public projectNameCounts: Map | null = null; public sortedEntries: Array<{ key: string; value: number }> = []; public sortColumn: SortColumn = 'name'; public sortDirection: SortDirection = 'asc'; public isDisabledByUserFilter = false; public filterUserId: string; + public confirmationChecked = false; // Two-phase UI state public phase: DialogPhase = 'input'; @@ -96,6 +100,7 @@ export class ProjectNameDialogComponent { } }); this.projectNameCounts = counts.size > 0 ? counts : null; + this.projectCount = counts.size; this.updateSortedEntries(); } diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json index e6e877a93..4550a3034 100644 --- a/src/assets/i18n/de.json +++ b/src/assets/i18n/de.json @@ -105,6 +105,8 @@ "CAMPAIGN_SELECTOR": "Kampagnen-Selektor", "CANCEL": "Abbrechen", "CANNOT_RENAME_PROJECTS_FOR": "Sie können Projekte nicht umbenennen für", + "CONFIRM_RENAME_MULTIPLE": "Ja! Ich möchte wirklich diese {{projectCount}} Projekte und {{jobCount}} Jobs umbenennen.", + "CONFIRM_RENAME_SINGLE": "Ja! Ich möchte wirklich dieses 1 Projekt und {{jobCount}} Jobs umbenennen.", "CENTER_COLUMN_AND_FILES_COLUMN_RIGHT_WILL_POPULATE": "(mittlere Spalte) und Dateispalte (rechts) werden ausgefüllt.", "CHART": "Diagramm", "CHEVRON_RIGHT": "chevron_right", diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 4ea8a3fc9..4c7f16853 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -124,6 +124,8 @@ "CANADAS_OPEN_LICENSE": "Canada's Open Government License", "CANCEL": "Cancel", "CANNOT_RENAME_PROJECTS_FOR": "You cannot rename projects for", + "CONFIRM_RENAME_MULTIPLE": "Yes! I really want to rename these {{projectCount}} projects and {{jobCount}} jobs.", + "CONFIRM_RENAME_SINGLE": "Yes! I really want to rename this 1 project and {{jobCount}} jobs.", "CENTER_COLUMN_AND_FILES_COLUMN_RIGHT_WILL_POPULATE": "(center column) and Files column (right) will populate.", "CHARACTERS": "characters", "CHART": "Chart", diff --git a/src/assets/i18n/es.json b/src/assets/i18n/es.json index 05b1e19f0..49f64679e 100644 --- a/src/assets/i18n/es.json +++ b/src/assets/i18n/es.json @@ -124,6 +124,8 @@ "CANADAS_OPEN_LICENSE": "Licencia de Gobierno Abierto de Canadá", "CANCEL": "Cancelar", "CANNOT_RENAME_PROJECTS_FOR": "No puede cambiar el nombre de los proyectos de", + "CONFIRM_RENAME_MULTIPLE": "¡Sí! Realmente quiero renombrar estos {{projectCount}} proyectos y {{jobCount}} trabajos.", + "CONFIRM_RENAME_SINGLE": "¡Sí! Realmente quiero renombrar este 1 proyecto y {{jobCount}} trabajos.", "CENTER_COLUMN_AND_FILES_COLUMN_RIGHT_WILL_POPULATE": "(columna central) y la columna Archivos (derecha) se completarán.", "CHARACTERS": "caracteres", "CHART": "Gráfico", From 425f88b28f150a9396aaf11b4cd3cb016ff9275a Mon Sep 17 00:00:00 2001 From: Andrew Anderson Date: Thu, 18 Dec 2025 14:56:59 -0800 Subject: [PATCH 29/59] UI Improvements * Auto-focus the input field * Show the new name in the confirmation message --- .../project-name-dialog.component.html | 4 ++-- .../project-name-dialog.component.ts | 21 +++++++++++++++++-- src/assets/i18n/de.json | 4 ++-- src/assets/i18n/en.json | 4 ++-- src/assets/i18n/es.json | 4 ++-- 5 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.html b/src/app/components/shared/project-name-dialog/project-name-dialog.component.html index e902c755b..db7fdfcfc 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.html +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.html @@ -19,10 +19,10 @@

{{ 'PROJECT_NAME' | translate }} @@ -78,7 +78,7 @@

[(ngModel)]="confirmationChecked" name="confirmationChecked" > - {{ (projectCount === 1 ? 'CONFIRM_RENAME_SINGLE' : 'CONFIRM_RENAME_MULTIPLE') | translate: { projectCount: projectCount, jobCount: jobCount } }} + {{ (projectCount === 1 ? 'CONFIRM_RENAME_SINGLE' : 'CONFIRM_RENAME_MULTIPLE') | translate: { projectCount: projectCount, jobCount: jobCount, newName: projectName?.trim() } }} } } diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts index 38cc842f2..b61a5b99d 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts @@ -1,4 +1,10 @@ -import { Component, inject } from '@angular/core'; +import { + Component, + inject, + AfterViewInit, + ViewChild, + ElementRef, +} from '@angular/core'; import { FormsModule } from '@angular/forms'; import { MatDialogRef, @@ -54,7 +60,9 @@ export interface ProjectNameDialogResult { TranslateModule, ], }) -export class ProjectNameDialogComponent { +export class ProjectNameDialogComponent implements AfterViewInit { + @ViewChild('projectNameInput') projectNameInput: ElementRef; + dialogRef = inject>(MatDialogRef); data = inject(MAT_DIALOG_DATA); private hyp3Api = inject(Hyp3ApiService); @@ -108,6 +116,15 @@ export class ProjectNameDialogComponent { this.dialogRef.disableClose = false; } + ngAfterViewInit(): void { + // Auto-focus the input field after the view is initialized + setTimeout(() => { + if (this.projectNameInput && !this.isDisabledByUserFilter) { + this.projectNameInput.nativeElement.focus(); + } + }); + } + public sortBy(column: SortColumn): void { if (this.sortColumn === column) { this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc'; diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json index 4550a3034..51ceccf8e 100644 --- a/src/assets/i18n/de.json +++ b/src/assets/i18n/de.json @@ -105,8 +105,8 @@ "CAMPAIGN_SELECTOR": "Kampagnen-Selektor", "CANCEL": "Abbrechen", "CANNOT_RENAME_PROJECTS_FOR": "Sie können Projekte nicht umbenennen für", - "CONFIRM_RENAME_MULTIPLE": "Ja! Ich möchte wirklich diese {{projectCount}} Projekte und {{jobCount}} Jobs umbenennen.", - "CONFIRM_RENAME_SINGLE": "Ja! Ich möchte wirklich dieses 1 Projekt und {{jobCount}} Jobs umbenennen.", + "CONFIRM_RENAME_MULTIPLE": "Ja! Ich möchte wirklich diese {{projectCount}} Projekte ({{jobCount}} Jobs) in \"{{newName}}\" umbenennen.", + "CONFIRM_RENAME_SINGLE": "Ja! Ich möchte wirklich dieses 1 Projekt ({{jobCount}} Jobs) in \"{{newName}}\" umbenennen.", "CENTER_COLUMN_AND_FILES_COLUMN_RIGHT_WILL_POPULATE": "(mittlere Spalte) und Dateispalte (rechts) werden ausgefüllt.", "CHART": "Diagramm", "CHEVRON_RIGHT": "chevron_right", diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 4c7f16853..0dd8c397b 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -124,8 +124,8 @@ "CANADAS_OPEN_LICENSE": "Canada's Open Government License", "CANCEL": "Cancel", "CANNOT_RENAME_PROJECTS_FOR": "You cannot rename projects for", - "CONFIRM_RENAME_MULTIPLE": "Yes! I really want to rename these {{projectCount}} projects and {{jobCount}} jobs.", - "CONFIRM_RENAME_SINGLE": "Yes! I really want to rename this 1 project and {{jobCount}} jobs.", + "CONFIRM_RENAME_MULTIPLE": "Yes! I really want to rename these {{projectCount}} projects ({{jobCount}} jobs) to \"{{newName}}\".", + "CONFIRM_RENAME_SINGLE": "Yes! I really want to rename this 1 project ({{jobCount}} jobs) to \"{{newName}}\".", "CENTER_COLUMN_AND_FILES_COLUMN_RIGHT_WILL_POPULATE": "(center column) and Files column (right) will populate.", "CHARACTERS": "characters", "CHART": "Chart", diff --git a/src/assets/i18n/es.json b/src/assets/i18n/es.json index 49f64679e..27982ca3b 100644 --- a/src/assets/i18n/es.json +++ b/src/assets/i18n/es.json @@ -124,8 +124,8 @@ "CANADAS_OPEN_LICENSE": "Licencia de Gobierno Abierto de Canadá", "CANCEL": "Cancelar", "CANNOT_RENAME_PROJECTS_FOR": "No puede cambiar el nombre de los proyectos de", - "CONFIRM_RENAME_MULTIPLE": "¡Sí! Realmente quiero renombrar estos {{projectCount}} proyectos y {{jobCount}} trabajos.", - "CONFIRM_RENAME_SINGLE": "¡Sí! Realmente quiero renombrar este 1 proyecto y {{jobCount}} trabajos.", + "CONFIRM_RENAME_MULTIPLE": "¡Sí! Realmente quiero renombrar estos {{projectCount}} proyectos ({{jobCount}} trabajos) a \"{{newName}}\".", + "CONFIRM_RENAME_SINGLE": "¡Sí! Realmente quiero renombrar este 1 proyecto ({{jobCount}} trabajos) a \"{{newName}}\".", "CENTER_COLUMN_AND_FILES_COLUMN_RIGHT_WILL_POPULATE": "(columna central) y la columna Archivos (derecha) se completarán.", "CHARACTERS": "caracteres", "CHART": "Gráfico", From 2951d5e61030a1a13568bbdb7a808f87709f0b8a Mon Sep 17 00:00:00 2001 From: Andrew Anderson Date: Thu, 18 Dec 2025 15:14:42 -0800 Subject: [PATCH 30/59] Show specific project names if failed I've implemented the feature to show which specific project names failed to rename in the completion phase. --- .../project-name-dialog.component.html | 13 +++++++ .../project-name-dialog.component.scss | 31 ++++++++++++++++ .../project-name-dialog.component.ts | 4 ++- src/app/services/hyp3/hyp3-api.service.ts | 35 ++++++++++++++----- src/assets/i18n/de.json | 1 + src/assets/i18n/en.json | 1 + src/assets/i18n/es.json | 1 + 7 files changed, 77 insertions(+), 9 deletions(-) diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.html b/src/app/components/shared/project-name-dialog/project-name-dialog.component.html index db7fdfcfc..81ef7b455 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.html +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.html @@ -137,6 +137,19 @@

{{ 'RENAME_COMPLETE' | translate }}

{{ 'RENAME_PARTIAL_SUCCESS' | translate: { success: successCount, failed: failedCount } }}

} + + @if (failedProjectNames.length > 0) { +
+

+ {{ 'FAILED_PROJECTS' | translate }}: +

+
    + @for (name of failedProjectNames; track name) { +
  • {{ name }}
  • + } +
+
+ }

diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss b/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss index 275f369c2..8fc55dd6a 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss @@ -162,3 +162,34 @@ color: #f44336; } } + +// Failed projects list +.failed-projects { + margin-top: 16px; + text-align: left; + + .failed-projects-label { + font-size: 14px; + font-weight: 500; + color: #f44336; + margin-bottom: 8px; + } + + .failed-projects-list { + margin: 0; + padding-left: 20px; + font-size: 13px; + border: 1px solid rgba(0, 0, 0, 0.12); + border-radius: 4px; + padding: 8px 8px 8px 28px; + + &.scrollable { + max-height: 120px; + overflow-y: auto; + } + + li { + padding: 2px 0; + } + } +} diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts index b61a5b99d..5542357dc 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts @@ -83,6 +83,7 @@ export class ProjectNameDialogComponent implements AfterViewInit { public progress = 0; public successCount = 0; public failedCount = 0; + public failedProjectNames: string[] = []; private subscriptions: Subscription[] = []; @@ -195,9 +196,10 @@ export class ProjectNameDialogComponent implements AfterViewInit { // Subscribe to final result this.subscriptions.push( result$.subscribe({ - next: ({ success, failed }) => { + next: ({ success, failed, failedProjectNames }) => { this.successCount = success; this.failedCount = failed; + this.failedProjectNames = failedProjectNames; this.phase = 'complete'; this.dialogRef.disableClose = false; }, diff --git a/src/app/services/hyp3/hyp3-api.service.ts b/src/app/services/hyp3/hyp3-api.service.ts index dab54c11d..d7ac502f9 100644 --- a/src/app/services/hyp3/hyp3-api.service.ts +++ b/src/app/services/hyp3/hyp3-api.service.ts @@ -215,6 +215,15 @@ export class Hyp3ApiService { ); } + /** + * Result type for rename operations with progress tracking. + */ + public static readonly RenameResult = class { + success: number; + failed: number; + failedProjectNames: string[]; + }; + /** * Updates job names with progress reporting. * Returns an object with: @@ -226,11 +235,10 @@ export class Hyp3ApiService { newProjectName: string, ): { progress$: Observable; - result$: Observable<{ success: number; failed: number }>; + result$: Observable<{ success: number; failed: number; failedProjectNames: string[] }>; } { const url = `${this.apiUrl}/jobs`; - const jobIds = products.map((product) => product.metadata.job.job_id); - const totalJobs = jobIds.length; + const totalJobs = products.length; const batchSize = 100; const totalBatches = Math.ceil(totalJobs / batchSize); @@ -242,10 +250,12 @@ export class Hyp3ApiService { let completedBatches = 0; let successCount = 0; let failedCount = 0; + const failedProjectNamesSet = new Set(); - const result$ = from(jobIds).pipe( + const result$ = from(products).pipe( bufferCount(batchSize), - mergeMap((jobIdsBatch) => { + mergeMap((productsBatch) => { + const jobIdsBatch = productsBatch.map((p) => p.metadata.job.job_id); return this.http .patch( url, @@ -253,19 +263,28 @@ export class Hyp3ApiService { { withCredentials: true }, ) .pipe( - map(() => ({ success: jobIdsBatch.length, failed: 0 })), - catchError(() => of({ success: 0, failed: jobIdsBatch.length })), + map(() => ({ success: jobIdsBatch.length, failed: 0, failedProducts: [] as models.CMRProduct[] })), + catchError(() => of({ success: 0, failed: jobIdsBatch.length, failedProducts: productsBatch })), tap((batchResult) => { completedBatches++; successCount += batchResult.success; failedCount += batchResult.failed; + // Track failed project names + batchResult.failedProducts.forEach((product) => { + const projectName = product.metadata?.job?.name || '(unnamed)'; + failedProjectNamesSet.add(projectName); + }); const progress = Math.round((completedBatches / totalBatches) * 100); progressSubject.next(progress); }), ); }, 3), toArray(), - map(() => ({ success: successCount, failed: failedCount })), + map(() => ({ + success: successCount, + failed: failedCount, + failedProjectNames: Array.from(failedProjectNamesSet).sort(), + })), finalize(() => progressSubject.complete()), ); diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json index 51ceccf8e..577ad5eb6 100644 --- a/src/assets/i18n/de.json +++ b/src/assets/i18n/de.json @@ -260,6 +260,7 @@ "EXPORT_API": "API exportieren", "EXPORT_PYTHON": "Python exportieren", "FAILED": "Misslungen", + "FAILED_PROJECTS": "Fehlgeschlagene Projekte", "FALSE": "},false);", "FARADAY_ROTATION": "Faraday-Rotation", "FEEDBACK": "Feedback", diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 0dd8c397b..51bfbd1f1 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -313,6 +313,7 @@ "EXPORT_API": "Export API", "EXPORT_PYTHON": "Export Python", "FAILED": "Failed", + "FAILED_PROJECTS": "Failed projects", "FALSE": "},false);", "FARADAY_ROTATION": "Faraday Rotation", "FEEDBACK": " Feedback", diff --git a/src/assets/i18n/es.json b/src/assets/i18n/es.json index 27982ca3b..85d8500c2 100644 --- a/src/assets/i18n/es.json +++ b/src/assets/i18n/es.json @@ -313,6 +313,7 @@ "EXPORT_API": "Exportar API", "EXPORT_PYTHON": "Exportar Python", "FAILED": "Fallido", + "FAILED_PROJECTS": "Proyectos fallidos", "FALSE": "},falso);", "FARADAY_ROTATION": "Rotación de Faraday", "FEEDBACK": "Comentarios", From a564ca109e651d4feae590cd3f863a6b7a6f510a Mon Sep 17 00:00:00 2001 From: Andrew Anderson Date: Thu, 18 Dec 2025 15:20:23 -0800 Subject: [PATCH 31/59] Show Time Estimate for Bulk Rename Completion I've implemented the estimated time remaining feature for large batch rename operations. Here's what was added: How it works: The estimated time is calculated based on the elapsed time and number of completed batches It only displays for operations with 1000+ jobs (to avoid showing for small, quick operations) The time is displayed in a user-friendly format (e.g., "< 1 min", "~1 min", "~5 min") --- .../project-name-dialog.component.html | 5 ++++ .../project-name-dialog.component.scss | 6 ++++ .../project-name-dialog.component.ts | 29 ++++++++++++++++-- src/app/services/hyp3/hyp3-api.service.ts | 30 ++++++++++++------- src/assets/i18n/de.json | 1 + src/assets/i18n/en.json | 1 + src/assets/i18n/es.json | 1 + 7 files changed, 61 insertions(+), 12 deletions(-) diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.html b/src/app/components/shared/project-name-dialog/project-name-dialog.component.html index 81ef7b455..b36be3455 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.html +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.html @@ -108,6 +108,11 @@

{{ 'RENAMING_JOBS' | translate }}

[value]="progress" >

{{ progress }}%

+ @if (formattedTimeRemaining) { +

+ {{ 'ESTIMATED_TIME_REMAINING' | translate }}: {{ formattedTimeRemaining }} +

+ }
diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss b/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss index 8fc55dd6a..d34092b02 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss @@ -138,6 +138,12 @@ font-size: 18px; font-weight: 500; } + + .estimated-time { + margin: 8px 0 0 0; + font-size: 13px; + opacity: 0.7; + } } // Result styles diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts index 5542357dc..592b53370 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts @@ -81,6 +81,7 @@ export class ProjectNameDialogComponent implements AfterViewInit { // Two-phase UI state public phase: DialogPhase = 'input'; public progress = 0; + public estimatedSecondsRemaining: number | null = null; public successCount = 0; public failedCount = 0; public failedProjectNames: string[] = []; @@ -159,6 +160,29 @@ export class ProjectNameDialogComponent implements AfterViewInit { return this.projectName?.trim().length > 0; } + /** + * Returns the estimated time remaining formatted as a human-readable string. + * Only shows for large operations (1000+ jobs). + */ + public get formattedTimeRemaining(): string | null { + // Only show estimated time for large operations + if (this.jobCount < 1000 || this.estimatedSecondsRemaining === null) { + return null; + } + + const seconds = this.estimatedSecondsRemaining; + if (seconds < 60) { + return `< 1 min`; + } + + const minutes = Math.ceil(seconds / 60); + if (minutes === 1) { + return `~1 min`; + } + + return `~${minutes} min`; + } + public onCancel(): void { this.dialogRef.close(); } @@ -188,8 +212,9 @@ export class ProjectNameDialogComponent implements AfterViewInit { // Subscribe to progress updates this.subscriptions.push( - progress$.subscribe((progress) => { - this.progress = progress; + progress$.subscribe(({ percent, estimatedSecondsRemaining }) => { + this.progress = percent; + this.estimatedSecondsRemaining = estimatedSecondsRemaining; }), ); diff --git a/src/app/services/hyp3/hyp3-api.service.ts b/src/app/services/hyp3/hyp3-api.service.ts index d7ac502f9..8d273c01d 100644 --- a/src/app/services/hyp3/hyp3-api.service.ts +++ b/src/app/services/hyp3/hyp3-api.service.ts @@ -216,25 +216,24 @@ export class Hyp3ApiService { } /** - * Result type for rename operations with progress tracking. + * Progress info emitted during rename operations. */ - public static readonly RenameResult = class { - success: number; - failed: number; - failedProjectNames: string[]; + public static readonly RenameProgressInfo = class { + percent: number; + estimatedSecondsRemaining: number | null; }; /** * Updates job names with progress reporting. * Returns an object with: - * - progress$: Observable that emits progress percentage (0-100) as batches complete + * - progress$: Observable that emits progress info (percent and estimated time) as batches complete * - result$: Observable that emits the final result when all batches are done */ public updateJobsNameWithProgress$( products: models.CMRProduct[], newProjectName: string, ): { - progress$: Observable; + progress$: Observable<{ percent: number; estimatedSecondsRemaining: number | null }>; result$: Observable<{ success: number; failed: number; failedProjectNames: string[] }>; } { const url = `${this.apiUrl}/jobs`; @@ -246,11 +245,12 @@ export class Hyp3ApiService { newProjectName = null; } - const progressSubject = new Subject(); + const progressSubject = new Subject<{ percent: number; estimatedSecondsRemaining: number | null }>(); let completedBatches = 0; let successCount = 0; let failedCount = 0; const failedProjectNamesSet = new Set(); + const startTime = Date.now(); const result$ = from(products).pipe( bufferCount(batchSize), @@ -274,8 +274,18 @@ export class Hyp3ApiService { const projectName = product.metadata?.job?.name || '(unnamed)'; failedProjectNamesSet.add(projectName); }); - const progress = Math.round((completedBatches / totalBatches) * 100); - progressSubject.next(progress); + const percent = Math.round((completedBatches / totalBatches) * 100); + + // Calculate estimated time remaining + let estimatedSecondsRemaining: number | null = null; + if (completedBatches > 0) { + const elapsedMs = Date.now() - startTime; + const msPerBatch = elapsedMs / completedBatches; + const remainingBatches = totalBatches - completedBatches; + estimatedSecondsRemaining = Math.ceil((msPerBatch * remainingBatches) / 1000); + } + + progressSubject.next({ percent, estimatedSecondsRemaining }); }), ); }, 3), diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json index 577ad5eb6..70b5bafd4 100644 --- a/src/assets/i18n/de.json +++ b/src/assets/i18n/de.json @@ -242,6 +242,7 @@ "ENVIRONMENT_FILE": "Umgebungsdatei", "EQUATORIAL_MAP_VIEW": "Äquatoriale Kartenansicht", "ERROR": "Fehler", + "ESTIMATED_TIME_REMAINING": "Geschätzte verbleibende Zeit", "ERS_DESC": "Hauptsächlich SAR-Bilder innerhalb der ASF und der McMurdo-Stationsmasken in der Antarktis. Die Daten sind für die Interferometrie geeignet.", "EVENT": "Ereignis", "EVENT_DATE": "Veranstaltungstermin", diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 51bfbd1f1..809d0d28b 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -295,6 +295,7 @@ "ENVIRONMENT_FILE": "Environment File", "EQUATORIAL_MAP_VIEW": "Equatorial Map View", "ERROR": "Error", + "ESTIMATED_TIME_REMAINING": "Estimated time remaining", "ERS_DESC": "Primarily SAR imagery within the ASF and the McMurdo, Antarctica, station masks. Data is suitable for interferometry.", "EVENT": "Event", "EVENT_DATE": "Event Date", diff --git a/src/assets/i18n/es.json b/src/assets/i18n/es.json index 85d8500c2..09e354613 100644 --- a/src/assets/i18n/es.json +++ b/src/assets/i18n/es.json @@ -295,6 +295,7 @@ "ENVIRONMENT_FILE": "Archivo de entorno", "EQUATORIAL_MAP_VIEW": "Ver Mapa Ecuatorial", "ERROR": "Error", + "ESTIMATED_TIME_REMAINING": "Tiempo restante estimado", "ERS_DESC": "Principalmente imágenes SAR dentro de las máscaras de la estación ASF y McMurdo, Antártida. Los datos son adecuados para la interferometría.", "EVENT": "Evento", "EVENT_DATE": "Fecha del evento", From 712411a5a0a847f09210d57160a99ee0130333b8 Mon Sep 17 00:00:00 2001 From: Andrew Anderson Date: Thu, 18 Dec 2025 15:47:32 -0800 Subject: [PATCH 32/59] Project Rename Title Updated Updated the table to William's suggestions. --- .../project-name-dialog/project-name-dialog.component.html | 4 ++-- .../project-name-dialog/project-name-dialog.component.scss | 5 +++++ src/assets/i18n/de.json | 1 + src/assets/i18n/en.json | 1 + src/assets/i18n/es.json | 1 + 5 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.html b/src/app/components/shared/project-name-dialog/project-name-dialog.component.html index b36be3455..c597aa205 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.html +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.html @@ -33,8 +33,8 @@

@if (projectNameCounts && projectNameCounts.size > 0) { -

- {{ 'PROJECTS_TO_RENAME' | translate }} +

+ {{ 'PROJECTS_SELECTED' | translate }}

Date: Thu, 18 Dec 2025 15:54:26 -0800 Subject: [PATCH 33/59] npm run lint -- --fix --- .../project-name-dialog.component.html | 45 ++++++++++++++--- .../project-name-dialog.component.ts | 5 +- src/app/services/hyp3/hyp3-api.service.ts | 49 ++++++++++++++++--- 3 files changed, 82 insertions(+), 17 deletions(-) diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.html b/src/app/components/shared/project-name-dialog/project-name-dialog.component.html index c597aa205..473208562 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.html +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.html @@ -46,13 +46,17 @@

{{ 'PROJECT_NAME' | translate }} @if (sortColumn === 'name') { - {{ sortDirection === 'asc' ? '▲' : '▼' }} + {{ + sortDirection === 'asc' ? '▲' : '▼' + }} } {{ 'JOBS' | translate }} @if (sortColumn === 'count') { - {{ sortDirection === 'asc' ? '▲' : '▼' }} + {{ + sortDirection === 'asc' ? '▲' : '▼' + }} } @@ -78,7 +82,18 @@

[(ngModel)]="confirmationChecked" name="confirmationChecked" > - {{ (projectCount === 1 ? 'CONFIRM_RENAME_SINGLE' : 'CONFIRM_RENAME_MULTIPLE') | translate: { projectCount: projectCount, jobCount: jobCount, newName: projectName?.trim() } }} + {{ + (projectCount === 1 + ? 'CONFIRM_RENAME_SINGLE' + : 'CONFIRM_RENAME_MULTIPLE' + ) + | translate + : { + projectCount: projectCount, + jobCount: jobCount, + newName: projectName?.trim(), + } + }} } } @@ -88,7 +103,16 @@

- @@ -110,7 +134,8 @@

{{ 'RENAMING_JOBS' | translate }}

{{ progress }}%

@if (formattedTimeRemaining) {

- {{ 'ESTIMATED_TIME_REMAINING' | translate }}: {{ formattedTimeRemaining }} + {{ 'ESTIMATED_TIME_REMAINING' | translate }}: + {{ formattedTimeRemaining }}

}
@@ -139,7 +164,10 @@

{{ 'RENAME_COMPLETE' | translate }}

} @else {

- {{ 'RENAME_PARTIAL_SUCCESS' | translate: { success: successCount, failed: failedCount } }} + {{ + 'RENAME_PARTIAL_SUCCESS' + | translate: { success: successCount, failed: failedCount } + }}

} @@ -148,7 +176,10 @@

{{ 'RENAME_COMPLETE' | translate }}

{{ 'FAILED_PROJECTS' | translate }}:

-
    +
      @for (name of failedProjectNames; track name) {
    • {{ name }}
    • } diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts index 592b53370..2b4d3df8f 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts @@ -4,6 +4,7 @@ import { AfterViewInit, ViewChild, ElementRef, + OnDestroy, } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { @@ -60,7 +61,7 @@ export interface ProjectNameDialogResult { TranslateModule, ], }) -export class ProjectNameDialogComponent implements AfterViewInit { +export class ProjectNameDialogComponent implements AfterViewInit, OnDestroy { @ViewChild('projectNameInput') projectNameInput: ElementRef; dialogRef = inject>(MatDialogRef); @@ -71,7 +72,7 @@ export class ProjectNameDialogComponent implements AfterViewInit { public jobCount = null; public projectCount = 0; public projectNameCounts: Map | null = null; - public sortedEntries: Array<{ key: string; value: number }> = []; + public sortedEntries: { key: string; value: number }[] = []; public sortColumn: SortColumn = 'name'; public sortDirection: SortDirection = 'asc'; public isDisabledByUserFilter = false; diff --git a/src/app/services/hyp3/hyp3-api.service.ts b/src/app/services/hyp3/hyp3-api.service.ts index 8d273c01d..e9ee8f5c3 100644 --- a/src/app/services/hyp3/hyp3-api.service.ts +++ b/src/app/services/hyp3/hyp3-api.service.ts @@ -5,7 +5,16 @@ import { HttpParams, } from '@angular/common/http'; -import { Observable, of, first, catchError, map, forkJoin, from, Subject } from 'rxjs'; +import { + Observable, + of, + first, + catchError, + map, + forkJoin, + from, + Subject, +} from 'rxjs'; import { mergeMap, toArray, bufferCount, tap, finalize } from 'rxjs/operators'; import * as moment from 'moment'; @@ -233,8 +242,15 @@ export class Hyp3ApiService { products: models.CMRProduct[], newProjectName: string, ): { - progress$: Observable<{ percent: number; estimatedSecondsRemaining: number | null }>; - result$: Observable<{ success: number; failed: number; failedProjectNames: string[] }>; + progress$: Observable<{ + percent: number; + estimatedSecondsRemaining: number | null; + }>; + result$: Observable<{ + success: number; + failed: number; + failedProjectNames: string[]; + }>; } { const url = `${this.apiUrl}/jobs`; const totalJobs = products.length; @@ -245,7 +261,10 @@ export class Hyp3ApiService { newProjectName = null; } - const progressSubject = new Subject<{ percent: number; estimatedSecondsRemaining: number | null }>(); + const progressSubject = new Subject<{ + percent: number; + estimatedSecondsRemaining: number | null; + }>(); let completedBatches = 0; let successCount = 0; let failedCount = 0; @@ -263,8 +282,18 @@ export class Hyp3ApiService { { withCredentials: true }, ) .pipe( - map(() => ({ success: jobIdsBatch.length, failed: 0, failedProducts: [] as models.CMRProduct[] })), - catchError(() => of({ success: 0, failed: jobIdsBatch.length, failedProducts: productsBatch })), + map(() => ({ + success: jobIdsBatch.length, + failed: 0, + failedProducts: [] as models.CMRProduct[], + })), + catchError(() => + of({ + success: 0, + failed: jobIdsBatch.length, + failedProducts: productsBatch, + }), + ), tap((batchResult) => { completedBatches++; successCount += batchResult.success; @@ -274,7 +303,9 @@ export class Hyp3ApiService { const projectName = product.metadata?.job?.name || '(unnamed)'; failedProjectNamesSet.add(projectName); }); - const percent = Math.round((completedBatches / totalBatches) * 100); + const percent = Math.round( + (completedBatches / totalBatches) * 100, + ); // Calculate estimated time remaining let estimatedSecondsRemaining: number | null = null; @@ -282,7 +313,9 @@ export class Hyp3ApiService { const elapsedMs = Date.now() - startTime; const msPerBatch = elapsedMs / completedBatches; const remainingBatches = totalBatches - completedBatches; - estimatedSecondsRemaining = Math.ceil((msPerBatch * remainingBatches) / 1000); + estimatedSecondsRemaining = Math.ceil( + (msPerBatch * remainingBatches) / 1000, + ); } progressSubject.next({ percent, estimatedSecondsRemaining }); From 1963c2a0eda698110ebc8878373dd8f9cab2b098 Mon Sep 17 00:00:00 2001 From: Andrew Anderson Date: Thu, 18 Dec 2025 16:22:55 -0800 Subject: [PATCH 34/59] Update src/app/store/scenes/scenes.action.ts Co-authored-by: William Horn --- src/app/store/scenes/scenes.action.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/store/scenes/scenes.action.ts b/src/app/store/scenes/scenes.action.ts index 820a5065e..441789bd1 100644 --- a/src/app/store/scenes/scenes.action.ts +++ b/src/app/store/scenes/scenes.action.ts @@ -9,7 +9,6 @@ import { SarviewsEvent, SarviewsProduct, CMRProductsById, - // Hyp3Job, } from '@models'; import { PinnedProduct } from '@services/browse-map.service'; From a26c8cc63762d4b8352edbce18b9e39a73b695ce Mon Sep 17 00:00:00 2001 From: Andrew Anderson Date: Thu, 18 Dec 2025 16:27:13 -0800 Subject: [PATCH 35/59] Update src/app/services/hyp3/hyp3-api.service.ts Co-authored-by: William Horn --- src/app/services/hyp3/hyp3-api.service.ts | 37 ----------------------- 1 file changed, 37 deletions(-) diff --git a/src/app/services/hyp3/hyp3-api.service.ts b/src/app/services/hyp3/hyp3-api.service.ts index e9ee8f5c3..6c34c550d 100644 --- a/src/app/services/hyp3/hyp3-api.service.ts +++ b/src/app/services/hyp3/hyp3-api.service.ts @@ -186,43 +186,6 @@ export class Hyp3ApiService { .pipe(map((resp) => resp as models.Hyp3Job)); } - public updateJobsName$( - products: models.CMRProduct[], - newProjectName: string, - ): Observable<{ success: number; failed: number }> { - const url = `${this.apiUrl}/jobs`; - const jobIds = products.map((product) => product.metadata.job.job_id); - - if (!newProjectName) { - newProjectName = null; - } - - return from(jobIds).pipe( - bufferCount(100), - mergeMap((jobIdsBatch) => { - return this.http - .patch( - url, - { name: newProjectName, job_ids: jobIdsBatch }, - { withCredentials: true }, - ) - .pipe( - map(() => ({ success: jobIdsBatch.length, failed: 0 })), - catchError(() => of({ success: 0, failed: jobIdsBatch.length })), - ); - }, 3), - toArray(), - map((results) => - results.reduce( - (acc, curr) => ({ - success: acc.success + curr.success, - failed: acc.failed + curr.failed, - }), - { success: 0, failed: 0 }, - ), - ), - ); - } /** * Progress info emitted during rename operations. From 522099230634e7331fb101ed66f337433a77fdf2 Mon Sep 17 00:00:00 2001 From: Andrew Anderson Date: Thu, 18 Dec 2025 16:42:00 -0800 Subject: [PATCH 36/59] Created three named interfaces at the top of hyp3-api.service.ts * RenameProgressInfo - Progress info with percent and estimated time remaining * RenameResult - Final result with success/failed counts and failed project names *RenameWithProgressResult - The composite return type containing both observables I also changed a 'sneaky' 3 into a constant --- src/app/services/hyp3/hyp3-api.service.ts | 44 +++++++++++------------ 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/src/app/services/hyp3/hyp3-api.service.ts b/src/app/services/hyp3/hyp3-api.service.ts index e9ee8f5c3..f25ae18b1 100644 --- a/src/app/services/hyp3/hyp3-api.service.ts +++ b/src/app/services/hyp3/hyp3-api.service.ts @@ -25,6 +25,22 @@ import { NotificationService } from '../notification.service'; import { Store } from '@ngrx/store'; import { AppState } from '@store'; +export interface RenameProgressInfo { + percent: number; + estimatedSecondsRemaining: number | null; +} + +export interface RenameResult { + success: number; + failed: number; + failedProjectNames: string[]; +} + +export interface RenameWithProgressResult { + progress$: Observable; + result$: Observable; +} + @Injectable({ providedIn: 'root', }) @@ -224,14 +240,6 @@ export class Hyp3ApiService { ); } - /** - * Progress info emitted during rename operations. - */ - public static readonly RenameProgressInfo = class { - percent: number; - estimatedSecondsRemaining: number | null; - }; - /** * Updates job names with progress reporting. * Returns an object with: @@ -241,30 +249,18 @@ export class Hyp3ApiService { public updateJobsNameWithProgress$( products: models.CMRProduct[], newProjectName: string, - ): { - progress$: Observable<{ - percent: number; - estimatedSecondsRemaining: number | null; - }>; - result$: Observable<{ - success: number; - failed: number; - failedProjectNames: string[]; - }>; - } { + ): RenameWithProgressResult { const url = `${this.apiUrl}/jobs`; const totalJobs = products.length; const batchSize = 100; + const concurrentBatches = 3; const totalBatches = Math.ceil(totalJobs / batchSize); if (!newProjectName) { newProjectName = null; } - const progressSubject = new Subject<{ - percent: number; - estimatedSecondsRemaining: number | null; - }>(); + const progressSubject = new Subject(); let completedBatches = 0; let successCount = 0; let failedCount = 0; @@ -321,7 +317,7 @@ export class Hyp3ApiService { progressSubject.next({ percent, estimatedSecondsRemaining }); }), ); - }, 3), + }, concurrentBatches), toArray(), map(() => ({ success: successCount, From 92cce687c622d3a77c2c95501dd87fe5a7192427 Mon Sep 17 00:00:00 2001 From: Andrew Anderson Date: Thu, 18 Dec 2025 16:44:29 -0800 Subject: [PATCH 37/59] Update src/app/components/shared/project-name-dialog/project-name-dialog.component.ts Co-authored-by: William Horn --- .../project-name-dialog/project-name-dialog.component.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts index 2b4d3df8f..ff463ac8c 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts @@ -177,9 +177,6 @@ export class ProjectNameDialogComponent implements AfterViewInit, OnDestroy { } const minutes = Math.ceil(seconds / 60); - if (minutes === 1) { - return `~1 min`; - } return `~${minutes} min`; } From dab41ed3e56b231d9b2196295301328b5acf086e Mon Sep 17 00:00:00 2001 From: Andrew Anderson Date: Fri, 19 Dec 2025 10:44:59 -0800 Subject: [PATCH 38/59] Tyler Suggestions Fixed - Red toast regression Fixed - Missing unnamed projects --- .../project-name-dialog.component.ts | 6 ++---- src/app/services/notification.service.ts | 13 ++----------- 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts index ff463ac8c..bf4071e59 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts @@ -105,10 +105,8 @@ export class ProjectNameDialogComponent implements AfterViewInit, OnDestroy { if (products) { const counts = new Map(); products.forEach((product) => { - const name = product.metadata?.job?.name; - if (name) { - counts.set(name, (counts.get(name) || 0) + 1); - } + const name = product.metadata?.job?.name || '(unnamed)'; + counts.set(name, (counts.get(name) || 0) + 1); }); this.projectNameCounts = counts.size > 0 ? counts : null; this.projectCount = counts.size; diff --git a/src/app/services/notification.service.ts b/src/app/services/notification.service.ts index 44dd5fdc4..a395d915b 100644 --- a/src/app/services/notification.service.ts +++ b/src/app/services/notification.service.ts @@ -181,17 +181,8 @@ export class NotificationService { title = '', options: Partial = {}, ): ActiveToast { - return this.toastr.error(message, title, { - ...options, - ...this.toastOptions, - }); - } - - public warn( - message: string, - title = '', - options: Partial = {}, - ): ActiveToast { + // Intentionally uses warning() instead of error() to avoid bright red toast + // which was deemed too visually jarring for the UX return this.toastr.warning(message, title, { ...options, ...this.toastOptions, From 7d7de8df4025ada7e6556c4496fbccff5c0f779b Mon Sep 17 00:00:00 2001 From: Andrew Anderson Date: Fri, 19 Dec 2025 10:48:51 -0800 Subject: [PATCH 39/59] Tyler Suggestions #2 Fixed - Empty string validation feedback Fixed - Checkbox disclaimer with empty string --- .../project-name-dialog/project-name-dialog.component.html | 7 +++++-- src/assets/i18n/de.json | 2 ++ src/assets/i18n/en.json | 1 + src/assets/i18n/es.json | 1 + 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.html b/src/app/components/shared/project-name-dialog/project-name-dialog.component.html index 473208562..b553effe8 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.html +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.html @@ -24,10 +24,13 @@

      [(ngModel)]="projectName" name="projectName" maxlength="100" + required [disabled]="isDisabledByUserFilter" /> {{ projectName?.length || 0 }}/100 - @if (projectName?.length >= 100) { + @if (!projectName?.trim()) { + {{ 'PROJECT_NAME_REQUIRED' | translate }} + } @else if (projectName?.length >= 100) { {{ 'PROJECT_NAME_MAX_LENGTH' | translate }} } @@ -76,7 +79,7 @@

- @if (!isDisabledByUserFilter) { + @if (!isDisabledByUserFilter && isValid) { Date: Fri, 19 Dec 2025 10:55:34 -0800 Subject: [PATCH 40/59] Tyler Suggestions #3 Fixed - Remove dialog service abstraction Fixed - Convert getter to signal/computed --- .../scene-file/scene-file.component.ts | 23 +++++-- .../scenes-list-header.component.ts | 46 ++++++++++---- .../project-name-dialog.component.html | 19 +++--- .../project-name-dialog.component.ts | 61 +++++++++---------- src/app/services/index.ts | 1 - .../services/project-name-dialog.service.ts | 57 ----------------- 6 files changed, 93 insertions(+), 114 deletions(-) delete mode 100644 src/app/services/project-name-dialog.service.ts diff --git a/src/app/components/results-menu/scene-files/scene-file/scene-file.component.ts b/src/app/components/results-menu/scene-files/scene-file/scene-file.component.ts index 80964e364..6f87995e0 100644 --- a/src/app/components/results-menu/scene-files/scene-file/scene-file.component.ts +++ b/src/app/components/results-menu/scene-files/scene-file/scene-file.component.ts @@ -17,7 +17,6 @@ import { EnvironmentService, Hyp3JobStatusService, OnDemandService, - ProjectNameDialogService, } from '@services'; import * as models from '@models'; import { SubSink } from 'subsink'; @@ -44,6 +43,11 @@ import { import { CopyToClipboardComponent } from '@components/shared/copy-to-clipboard/copy-to-clipboard.component'; import { MatIconButton } from '@angular/material/button'; import { MatMenuTrigger, MatMenu, MatMenuItem } from '@angular/material/menu'; +import { MatDialog } from '@angular/material/dialog'; +import { + ProjectNameDialogComponent, + ProjectNameDialogData, +} from '@components/shared/project-name-dialog'; import { DownloadFileButtonComponent } from '@components/shared/download-file-button/download-file-button.component'; import { CartToggleComponent } from '@components/shared/cart-toggle/cart-toggle.component'; import { Hyp3JobStatusBadgeComponent } from '@components/shared/hyp3-job-status-badge/hyp3-job-status-badge.component'; @@ -88,7 +92,7 @@ export class SceneFileComponent implements OnInit, OnDestroy { private store$ = inject>(Store); env = inject(EnvironmentService); private onDemand = inject(OnDemandService); - private projectNameDialog = inject(ProjectNameDialogService); + private dialog = inject(MatDialog); @Input() product: models.CMRProduct; @Input() isQueued: boolean; @@ -213,8 +217,19 @@ export class SceneFileComponent implements OnInit, OnDestroy { } public onEditProjectName(oldProjectName: string): void { - this.projectNameDialog.open(oldProjectName).subscribe((result) => { - if (result !== undefined && typeof result === 'string') { + const dialogRef = this.dialog.open< + ProjectNameDialogComponent, + ProjectNameDialogData, + string + >(ProjectNameDialogComponent, { + width: '400px', + data: { + currentName: oldProjectName, + }, + }); + + dialogRef.afterClosed().subscribe((result) => { + if (result !== undefined) { this.renameJobProjectName.emit(result); } }); diff --git a/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.ts b/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.ts index 473e5ac4b..b5da70a3f 100644 --- a/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.ts +++ b/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.ts @@ -32,7 +32,6 @@ import { SarviewsEventsService, NotificationService, ExportService, - ProjectNameDialogService, } from '@services'; import * as models from '@models'; @@ -45,6 +44,11 @@ import { CodeExportComponent, CodeExportType, } from '@components/shared/code-export'; +import { + ProjectNameDialogComponent, + ProjectNameDialogData, + ProjectNameDialogResult, +} from '@components/shared/project-name-dialog'; import { MatDialog } from '@angular/material/dialog'; import { NgClass, @@ -105,7 +109,6 @@ export class ScenesListHeaderComponent implements OnInit, OnDestroy { private possibleHyp3JobsService = inject(PossibleHyp3JobsService); private exportService = inject(ExportService); private dialog = inject(MatDialog); - private projectNameDialog = inject(ProjectNameDialogService); public copyIcon = faCopy; public pairs$ = this.pairService.pairs$; @@ -681,15 +684,36 @@ export class ScenesListHeaderComponent implements OnInit, OnDestroy { } public onBulkProjectNameUpdate(): void { - this.projectNameDialog.open('', this.products).subscribe((result) => { - if (result !== undefined && typeof result !== 'string') { - // Dialog now handles the rename operation and shows progress - // We just need to update the filter and refresh search - this.store$.dispatch(new searchStore.ClearSearch()); - this.store$.dispatch(new filtersStore.SetProjectName(result.newName)); - this.store$.dispatch(new searchStore.MakeSearch()); - } - }); + combineLatest([ + this.store$.select(hyp3Store.getHyp3User), + this.store$.select(hyp3Store.getOnDemandUserId), + ]) + .pipe(take(1)) + .subscribe(([hyp3User, filterUserId]) => { + const dialogRef = this.dialog.open< + ProjectNameDialogComponent, + ProjectNameDialogData, + ProjectNameDialogResult + >(ProjectNameDialogComponent, { + width: '400px', + data: { + currentName: '', + products: this.products, + loggedInUserId: hyp3User?.user_id, + filterUserId: filterUserId || undefined, + }, + }); + + dialogRef.afterClosed().subscribe((result) => { + if (result !== undefined) { + // Dialog handles the rename operation and shows progress + // We just need to update the filter and refresh search + this.store$.dispatch(new searchStore.ClearSearch()); + this.store$.dispatch(new filtersStore.SetProjectName(result.newName)); + this.store$.dispatch(new searchStore.MakeSearch()); + } + }); + }); } ngOnDestroy(): void { diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.html b/src/app/components/shared/project-name-dialog/project-name-dialog.component.html index b553effe8..98b63fdf7 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.html +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.html @@ -21,16 +21,17 @@

- {{ projectName?.length || 0 }}/100 - @if (!projectName?.trim()) { + {{ projectName()?.length || 0 }}/100 + @if (!projectName()?.trim()) { {{ 'PROJECT_NAME_REQUIRED' | translate }} - } @else if (projectName?.length >= 100) { + } @else if (projectName()?.length >= 100) { {{ 'PROJECT_NAME_MAX_LENGTH' | translate }} } @@ -79,7 +80,7 @@

- @if (!isDisabledByUserFilter && isValid) { + @if (!isDisabledByUserFilter && isValid()) { : { projectCount: projectCount, jobCount: jobCount, - newName: projectName?.trim(), + newName: projectName()?.trim(), } }} @@ -111,7 +112,7 @@

color="primary" type="submit" [disabled]=" - !isValid || + !isValid() || isDisabledByUserFilter || (projectNameCounts && !confirmationChecked) " @@ -135,10 +136,10 @@

{{ 'RENAMING_JOBS' | translate }}

[value]="progress" >

{{ progress }}%

- @if (formattedTimeRemaining) { + @if (formattedTimeRemaining()) {

{{ 'ESTIMATED_TIME_REMAINING' | translate }}: - {{ formattedTimeRemaining }} + {{ formattedTimeRemaining() }}

} diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts index bf4071e59..952b3619c 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts @@ -5,6 +5,8 @@ import { ViewChild, ElementRef, OnDestroy, + signal, + computed, } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { @@ -68,8 +70,9 @@ export class ProjectNameDialogComponent implements AfterViewInit, OnDestroy { data = inject(MAT_DIALOG_DATA); private hyp3Api = inject(Hyp3ApiService); - public projectName: string; - public jobCount = null; + // Use signals for reactive state + public projectName = signal(''); + public jobCount: number | null = null; public projectCount = 0; public projectNameCounts: Map | null = null; public sortedEntries: { key: string; value: number }[] = []; @@ -82,15 +85,33 @@ export class ProjectNameDialogComponent implements AfterViewInit, OnDestroy { // Two-phase UI state public phase: DialogPhase = 'input'; public progress = 0; - public estimatedSecondsRemaining: number | null = null; + public estimatedSecondsRemaining = signal(null); public successCount = 0; public failedCount = 0; public failedProjectNames: string[] = []; + // Computed values (only recalculate when dependencies change) + public isValid = computed(() => this.projectName().trim().length > 0); + + public formattedTimeRemaining = computed(() => { + // Only show estimated time for large operations (1000+ jobs) + if (this.jobCount < 1000 || this.estimatedSecondsRemaining() === null) { + return null; + } + + const seconds = this.estimatedSecondsRemaining(); + if (seconds < 60) { + return `< 1 min`; + } + + const minutes = Math.ceil(seconds / 60); + return `~${minutes} min`; + }); + private subscriptions: Subscription[] = []; constructor() { - this.projectName = this.data.currentName; + this.projectName.set(this.data.currentName); // Disable input if filtering by a different user's jobs const { loggedInUserId, filterUserId } = this.data; @@ -155,40 +176,16 @@ export class ProjectNameDialogComponent implements AfterViewInit, OnDestroy { }); } - public get isValid(): boolean { - return this.projectName?.trim().length > 0; - } - - /** - * Returns the estimated time remaining formatted as a human-readable string. - * Only shows for large operations (1000+ jobs). - */ - public get formattedTimeRemaining(): string | null { - // Only show estimated time for large operations - if (this.jobCount < 1000 || this.estimatedSecondsRemaining === null) { - return null; - } - - const seconds = this.estimatedSecondsRemaining; - if (seconds < 60) { - return `< 1 min`; - } - - const minutes = Math.ceil(seconds / 60); - - return `~${minutes} min`; - } - public onCancel(): void { this.dialogRef.close(); } public onSave(): void { - if (!this.isValid) { + if (!this.isValid()) { return; } - const trimmedName = this.projectName.trim(); + const trimmedName = this.projectName().trim(); // If no products, just return the name (single-file rename flow) if (!this.data.products || this.data.products.length === 0) { @@ -210,7 +207,7 @@ export class ProjectNameDialogComponent implements AfterViewInit, OnDestroy { this.subscriptions.push( progress$.subscribe(({ percent, estimatedSecondsRemaining }) => { this.progress = percent; - this.estimatedSecondsRemaining = estimatedSecondsRemaining; + this.estimatedSecondsRemaining.set(estimatedSecondsRemaining); }), ); @@ -234,7 +231,7 @@ export class ProjectNameDialogComponent implements AfterViewInit, OnDestroy { public onDone(): void { const result: ProjectNameDialogResult = { - newName: this.projectName.trim(), + newName: this.projectName().trim(), success: this.successCount, failed: this.failedCount, }; diff --git a/src/app/services/index.ts b/src/app/services/index.ts index 2b6018fff..f8a99d5da 100644 --- a/src/app/services/index.ts +++ b/src/app/services/index.ts @@ -43,4 +43,3 @@ export { ExportService } from './export.service'; export { NetcdfService } from './netcdf-service.service'; export { PointHistoryService } from './point-history.service'; export { FrameMapService } from './frame-map.service'; -export { ProjectNameDialogService } from './project-name-dialog.service'; diff --git a/src/app/services/project-name-dialog.service.ts b/src/app/services/project-name-dialog.service.ts deleted file mode 100644 index b9b1e5861..000000000 --- a/src/app/services/project-name-dialog.service.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { Injectable, inject } from '@angular/core'; -import { MatDialog } from '@angular/material/dialog'; -import { Store } from '@ngrx/store'; -import { Observable, take, combineLatest, switchMap } from 'rxjs'; - -import { - ProjectNameDialogComponent, - ProjectNameDialogData, - ProjectNameDialogResult, -} from '@components/shared/project-name-dialog'; -import { CMRProduct } from '@models'; -import { AppState } from '@store'; -import * as hyp3Store from '@store/hyp3'; - -@Injectable({ providedIn: 'root' }) -export class ProjectNameDialogService { - private dialog = inject(MatDialog); - private store$ = inject>(Store); - - /** - * Opens the project name dialog. - * - * When products are provided (bulk rename), the dialog handles the rename - * operation internally and returns ProjectNameDialogResult. - * - * When no products are provided (single-file rename), the dialog just - * returns the new name as a string. - */ - open( - currentName: string, - products?: CMRProduct[], - ): Observable { - return combineLatest([ - this.store$.select(hyp3Store.getHyp3User), - this.store$.select(hyp3Store.getOnDemandUserId), - ]).pipe( - take(1), - switchMap(([hyp3User, filterUserId]) => { - const dialogRef = this.dialog.open< - ProjectNameDialogComponent, - ProjectNameDialogData, - string | ProjectNameDialogResult - >(ProjectNameDialogComponent, { - width: '400px', - data: { - currentName, - products, - loggedInUserId: hyp3User?.user_id, - filterUserId: filterUserId || undefined, - }, - }); - - return dialogRef.afterClosed(); - }), - ); - } -} From 4567293a5aa43e013000fb64839397d9b46181e5 Mon Sep 17 00:00:00 2001 From: Andrew Anderson Date: Fri, 19 Dec 2025 10:59:26 -0800 Subject: [PATCH 41/59] Tyler Suggestions #4 Fixed - Use SubSink pattern consistently Fixed - Moved constructor logic to ngOnInit --- .../project-name-dialog.component.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts index 952b3619c..6e12c5826 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts @@ -5,6 +5,7 @@ import { ViewChild, ElementRef, OnDestroy, + OnInit, signal, computed, } from '@angular/core'; @@ -22,7 +23,7 @@ import { MatButtonModule } from '@angular/material/button'; import { MatProgressBarModule } from '@angular/material/progress-bar'; import { MatCheckboxModule } from '@angular/material/checkbox'; import { TranslateModule } from '@ngx-translate/core'; -import { Subscription } from 'rxjs'; +import { SubSink } from 'subsink'; import * as models from '@models'; import { Hyp3ApiService } from '@services'; @@ -63,7 +64,7 @@ export interface ProjectNameDialogResult { TranslateModule, ], }) -export class ProjectNameDialogComponent implements AfterViewInit, OnDestroy { +export class ProjectNameDialogComponent implements OnInit, AfterViewInit, OnDestroy { @ViewChild('projectNameInput') projectNameInput: ElementRef; dialogRef = inject>(MatDialogRef); @@ -108,9 +109,9 @@ export class ProjectNameDialogComponent implements AfterViewInit, OnDestroy { return `~${minutes} min`; }); - private subscriptions: Subscription[] = []; + private subs = new SubSink(); - constructor() { + ngOnInit(): void { this.projectName.set(this.data.currentName); // Disable input if filtering by a different user's jobs @@ -204,7 +205,7 @@ export class ProjectNameDialogComponent implements AfterViewInit, OnDestroy { ); // Subscribe to progress updates - this.subscriptions.push( + this.subs.add( progress$.subscribe(({ percent, estimatedSecondsRemaining }) => { this.progress = percent; this.estimatedSecondsRemaining.set(estimatedSecondsRemaining); @@ -212,7 +213,7 @@ export class ProjectNameDialogComponent implements AfterViewInit, OnDestroy { ); // Subscribe to final result - this.subscriptions.push( + this.subs.add( result$.subscribe({ next: ({ success, failed, failedProjectNames }) => { this.successCount = success; @@ -239,6 +240,6 @@ export class ProjectNameDialogComponent implements AfterViewInit, OnDestroy { } ngOnDestroy(): void { - this.subscriptions.forEach((sub) => sub.unsubscribe()); + this.subs.unsubscribe(); } } From 1b0824849e17b0c35313bc566d8143a563f181ad Mon Sep 17 00:00:00 2001 From: Andrew Anderson Date: Fri, 19 Dec 2025 11:15:32 -0800 Subject: [PATCH 42/59] Tyler Suggestions #5 Fixed - Material Table with Sorting in-place Reviewing the idea of using the Material Stepper consideration, I don't think it is a good fit. The phases are not user-navigated steps, they're system-driven and there is no option for backward navigation. --- .../project-name-dialog.component.html | 70 ++++++++-------- .../project-name-dialog.component.ts | 83 ++++++++++--------- 2 files changed, 80 insertions(+), 73 deletions(-) diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.html b/src/app/components/shared/project-name-dialog/project-name-dialog.component.html index 98b63fdf7..c69b3e5f5 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.html +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.html @@ -36,48 +36,46 @@

} - @if (projectNameCounts && projectNameCounts.size > 0) { + @if (dataSource.data.length > 0) {

{{ 'PROJECTS_SELECTED' | translate }}

- - - - - - - +
- {{ 'PROJECT_NAME' | translate }} - @if (sortColumn === 'name') { - {{ - sortDirection === 'asc' ? '▲' : '▼' - }} - } - - {{ 'JOBS' | translate }} - @if (sortColumn === 'count') { - {{ - sortDirection === 'asc' ? '▲' : '▼' - }} - } -
+ + + + + + + + + + + + + +
+ {{ 'PROJECT_NAME' | translate }} + {{ entry.name }} + {{ 'JOBS' | translate }} + + {{ entry.count }} +
-
- - - @for (entry of sortedEntries; track entry.key) { - - - - - } - -
{{ entry.key }}{{ entry.value }}
-
@if (!isDisabledByUserFilter && isValid()) { @@ -114,7 +112,7 @@

[disabled]=" !isValid() || isDisabledByUserFilter || - (projectNameCounts && !confirmationChecked) + (dataSource.data.length > 0 && !confirmationChecked) " > {{ 'SAVE' | translate }} diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts index 6e12c5826..39126a271 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts @@ -22,14 +22,30 @@ import { MatInputModule } from '@angular/material/input'; import { MatButtonModule } from '@angular/material/button'; import { MatProgressBarModule } from '@angular/material/progress-bar'; import { MatCheckboxModule } from '@angular/material/checkbox'; +import { + MatTableDataSource, + MatTable, + MatColumnDef, + MatHeaderCellDef, + MatHeaderCell, + MatCellDef, + MatCell, + MatHeaderRowDef, + MatHeaderRow, + MatRowDef, + MatRow, +} from '@angular/material/table'; +import { MatSort, MatSortHeader } from '@angular/material/sort'; import { TranslateModule } from '@ngx-translate/core'; import { SubSink } from 'subsink'; import * as models from '@models'; import { Hyp3ApiService } from '@services'; -type SortColumn = 'name' | 'count'; -type SortDirection = 'asc' | 'desc'; +export interface ProjectEntry { + name: string; + count: number; +} export type DialogPhase = 'input' | 'processing' | 'complete' | 'error'; @@ -61,11 +77,24 @@ export interface ProjectNameDialogResult { MatButtonModule, MatProgressBarModule, MatCheckboxModule, + MatTable, + MatSort, + MatSortHeader, + MatColumnDef, + MatHeaderCellDef, + MatHeaderCell, + MatCellDef, + MatCell, + MatHeaderRowDef, + MatHeaderRow, + MatRowDef, + MatRow, TranslateModule, ], }) export class ProjectNameDialogComponent implements OnInit, AfterViewInit, OnDestroy { @ViewChild('projectNameInput') projectNameInput: ElementRef; + @ViewChild(MatSort) sort: MatSort; dialogRef = inject>(MatDialogRef); data = inject(MAT_DIALOG_DATA); @@ -75,14 +104,14 @@ export class ProjectNameDialogComponent implements OnInit, AfterViewInit, OnDest public projectName = signal(''); public jobCount: number | null = null; public projectCount = 0; - public projectNameCounts: Map | null = null; - public sortedEntries: { key: string; value: number }[] = []; - public sortColumn: SortColumn = 'name'; - public sortDirection: SortDirection = 'asc'; public isDisabledByUserFilter = false; public filterUserId: string; public confirmationChecked = false; + // Material table with sorting + public dataSource = new MatTableDataSource([]); + public displayedColumns: string[] = ['name', 'count']; + // Two-phase UI state public phase: DialogPhase = 'input'; public progress = 0; @@ -130,9 +159,13 @@ export class ProjectNameDialogComponent implements OnInit, AfterViewInit, OnDest const name = product.metadata?.job?.name || '(unnamed)'; counts.set(name, (counts.get(name) || 0) + 1); }); - this.projectNameCounts = counts.size > 0 ? counts : null; + + // Convert to array for Material table + const entries: ProjectEntry[] = Array.from(counts.entries()).map( + ([name, count]) => ({ name, count }), + ); + this.dataSource.data = entries; this.projectCount = counts.size; - this.updateSortedEntries(); } // Prevent closing during processing @@ -140,6 +173,11 @@ export class ProjectNameDialogComponent implements OnInit, AfterViewInit, OnDest } ngAfterViewInit(): void { + // Connect MatSort to dataSource for automatic sorting + if (this.sort) { + this.dataSource.sort = this.sort; + } + // Auto-focus the input field after the view is initialized setTimeout(() => { if (this.projectNameInput && !this.isDisabledByUserFilter) { @@ -148,35 +186,6 @@ export class ProjectNameDialogComponent implements OnInit, AfterViewInit, OnDest }); } - public sortBy(column: SortColumn): void { - if (this.sortColumn === column) { - this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc'; - } else { - this.sortColumn = column; - this.sortDirection = 'asc'; - } - this.updateSortedEntries(); - } - - private updateSortedEntries(): void { - if (!this.projectNameCounts) { - this.sortedEntries = []; - return; - } - - this.sortedEntries = Array.from(this.projectNameCounts.entries()) - .map(([key, value]) => ({ key, value })) - .sort((a, b) => { - let comparison: number; - if (this.sortColumn === 'name') { - comparison = a.key.localeCompare(b.key); - } else { - comparison = a.value - b.value; - } - return this.sortDirection === 'asc' ? comparison : -comparison; - }); - } - public onCancel(): void { this.dialogRef.close(); } From 8eafa51ae5d98fd6c398f1b502f5d1baa666bc1f Mon Sep 17 00:00:00 2001 From: Andrew Anderson Date: Fri, 19 Dec 2025 11:17:51 -0800 Subject: [PATCH 43/59] npm run lint -- --fix --- .../scenes-list-header/scenes-list-header.component.ts | 4 +++- .../project-name-dialog/project-name-dialog.component.ts | 4 +++- src/app/services/hyp3/hyp3-api.service.ts | 1 - 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.ts b/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.ts index b5da70a3f..7ecc22dd7 100644 --- a/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.ts +++ b/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.ts @@ -709,7 +709,9 @@ export class ScenesListHeaderComponent implements OnInit, OnDestroy { // Dialog handles the rename operation and shows progress // We just need to update the filter and refresh search this.store$.dispatch(new searchStore.ClearSearch()); - this.store$.dispatch(new filtersStore.SetProjectName(result.newName)); + this.store$.dispatch( + new filtersStore.SetProjectName(result.newName), + ); this.store$.dispatch(new searchStore.MakeSearch()); } }); diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts index 39126a271..f92c1d20e 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts @@ -92,7 +92,9 @@ export interface ProjectNameDialogResult { TranslateModule, ], }) -export class ProjectNameDialogComponent implements OnInit, AfterViewInit, OnDestroy { +export class ProjectNameDialogComponent + implements OnInit, AfterViewInit, OnDestroy +{ @ViewChild('projectNameInput') projectNameInput: ElementRef; @ViewChild(MatSort) sort: MatSort; diff --git a/src/app/services/hyp3/hyp3-api.service.ts b/src/app/services/hyp3/hyp3-api.service.ts index bbc6f6792..401cc953f 100644 --- a/src/app/services/hyp3/hyp3-api.service.ts +++ b/src/app/services/hyp3/hyp3-api.service.ts @@ -202,7 +202,6 @@ export class Hyp3ApiService { .pipe(map((resp) => resp as models.Hyp3Job)); } - /** * Updates job names with progress reporting. * Returns an object with: From 26e50559e99fc28e9f533d4532380082e0497115 Mon Sep 17 00:00:00 2001 From: Andrew Anderson Date: Fri, 19 Dec 2025 11:37:24 -0800 Subject: [PATCH 44/59] Conformity & Best Practices #2 - Removed stale SCSS styles: Cleaned up .sortable, .sort-indicator, .projects-table-body-container, and custom table styling that was no longer needed after switching to Material table * Made dialog injects private: Changed dialogRef and data from public to private to match codebase pattern * Added readonly to displayedColumns: Added readonly modifier since the array never changes * Fixed track function: Changed track name to track $index to handle potential duplicate project names in the failed list --- .../project-name-dialog.component.html | 2 +- .../project-name-dialog.component.scss | 50 ++----------------- .../project-name-dialog.component.ts | 6 +-- 3 files changed, 7 insertions(+), 51 deletions(-) diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.html b/src/app/components/shared/project-name-dialog/project-name-dialog.component.html index c69b3e5f5..810103917 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.html +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.html @@ -182,7 +182,7 @@

{{ 'RENAME_COMPLETE' | translate }}

class="failed-projects-list" [class.scrollable]="failedProjectNames.length > 5" > - @for (name of failedProjectNames; track name) { + @for (name of failedProjectNames; track $index) {
  • {{ name }}
  • } diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss b/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss index e948ef3cc..db5dd100a 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss @@ -46,60 +46,16 @@ border: 1px solid rgba(0, 0, 0, 0.12); border-radius: 4px; margin-bottom: 8px; + overflow: hidden; &.scrollable { - .projects-table-body-container { - max-height: 185px; - overflow-y: scroll; - } - - > .projects-table .job-count-header { - padding-right: 29px; // 12px base + 17px scrollbar width - } + max-height: 220px; + overflow-y: auto; } } -.projects-table-body-container { - border-top: 1px solid rgba(0, 0, 0, 0.12); -} - .projects-table { width: 100%; - border-collapse: collapse; - - th, - td { - padding: 8px 12px; - font-size: 14px; - text-align: left; - } - - th { - font-weight: 500; - opacity: 0.7; - - &.sortable { - cursor: pointer; - user-select: none; - - &:hover { - opacity: 1; - } - } - } - - td { - border-top: 1px solid rgba(0, 0, 0, 0.12); - } - - tr:first-child td { - border-top: none; - } - - .sort-indicator { - font-size: 10px; - margin-left: 4px; - } .job-count-header, .job-count-cell { diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts index f92c1d20e..d773c8a59 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts @@ -98,8 +98,8 @@ export class ProjectNameDialogComponent @ViewChild('projectNameInput') projectNameInput: ElementRef; @ViewChild(MatSort) sort: MatSort; - dialogRef = inject>(MatDialogRef); - data = inject(MAT_DIALOG_DATA); + private dialogRef = inject>(MatDialogRef); + private data = inject(MAT_DIALOG_DATA); private hyp3Api = inject(Hyp3ApiService); // Use signals for reactive state @@ -112,7 +112,7 @@ export class ProjectNameDialogComponent // Material table with sorting public dataSource = new MatTableDataSource([]); - public displayedColumns: string[] = ['name', 'count']; + public readonly displayedColumns: string[] = ['name', 'count']; // Two-phase UI state public phase: DialogPhase = 'input'; From 41bb9d2cd199e969ba382f9ddc5bb82097766177 Mon Sep 17 00:00:00 2001 From: Andrew Anderson Date: Fri, 19 Dec 2025 11:56:36 -0800 Subject: [PATCH 45/59] Made "(unnamed)" multilingual --- .../project-name-dialog/project-name-dialog.component.ts | 6 ++++-- src/assets/i18n/de.json | 1 + src/assets/i18n/en.json | 1 + src/assets/i18n/es.json | 1 + 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts index d773c8a59..38e574f6b 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts @@ -36,7 +36,7 @@ import { MatRow, } from '@angular/material/table'; import { MatSort, MatSortHeader } from '@angular/material/sort'; -import { TranslateModule } from '@ngx-translate/core'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { SubSink } from 'subsink'; import * as models from '@models'; @@ -101,6 +101,7 @@ export class ProjectNameDialogComponent private dialogRef = inject>(MatDialogRef); private data = inject(MAT_DIALOG_DATA); private hyp3Api = inject(Hyp3ApiService); + private translateService = inject(TranslateService); // Use signals for reactive state public projectName = signal(''); @@ -156,9 +157,10 @@ export class ProjectNameDialogComponent this.jobCount = products?.length; if (products) { + const unnamedLabel = this.translateService.instant('PROJECT_NAME_UNNAMED'); const counts = new Map(); products.forEach((product) => { - const name = product.metadata?.job?.name || '(unnamed)'; + const name = product.metadata?.job?.name || unnamedLabel; counts.set(name, (counts.get(name) || 0) + 1); }); diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json index 4ed88c97e..710f679de 100644 --- a/src/assets/i18n/de.json +++ b/src/assets/i18n/de.json @@ -567,6 +567,7 @@ "PROJECT_NAME": "Projektname", "PROJECT_NAME_MAX_LENGTH": "Projektname darf 100 Zeichen nicht überschreiten", "PROJECT_NAME_REQUIRED": "Projektname ist erforderlich", + "PROJECT_NAME_UNNAMED": "(unbenannt)", "PROJECT_NAME_UPDATE_ALL_TOOLTIP": "Projektname für alle geladenen Aufträge aktualisieren", "PROJECT_NAME_UPDATE_FOR_JOBS": "Projektname für {{count}} Aufträge aktualisieren", "PROJECTS_SELECTED": "Ausgewählte Projekte", diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 5df400064..b88ab00a2 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -678,6 +678,7 @@ "PROJECT_NAME": "Project Name", "PROJECT_NAME_MAX_LENGTH": "Project name cannot exceed 100 characters", "PROJECT_NAME_REQUIRED": "Project name is required", + "PROJECT_NAME_UNNAMED": "(unnamed)", "PROJECT_NAME_UPDATE_ALL_TOOLTIP": "Update project name for all loaded jobs", "PROJECT_NAME_UPDATE_FOR_JOBS": "Update project name for {{count}} jobs", "PROJECTS_SELECTED": "Projects Selected", diff --git a/src/assets/i18n/es.json b/src/assets/i18n/es.json index 12a74b900..ba4d9b069 100644 --- a/src/assets/i18n/es.json +++ b/src/assets/i18n/es.json @@ -679,6 +679,7 @@ "PROJECT_NAME": "Nombre del Proyecto", "PROJECT_NAME_MAX_LENGTH": "El nombre del proyecto no puede exceder 100 caracteres", "PROJECT_NAME_REQUIRED": "El nombre del proyecto es obligatorio", + "PROJECT_NAME_UNNAMED": "(sin nombre)", "PROJECT_NAME_UPDATE_ALL_TOOLTIP": "Actualizar nombre del proyecto para todos los trabajos cargados", "PROJECT_NAME_UPDATE_FOR_JOBS": "Actualizar nombre del proyecto para {{count}} trabajos", "PROJECTS_SELECTED": "Proyectos Seleccionados", From 852061dc9e12167e0d3661beeeb98e90a829b78a Mon Sep 17 00:00:00 2001 From: Andrew Anderson Date: Fri, 19 Dec 2025 12:30:03 -0800 Subject: [PATCH 46/59] Tweaks Made the Project Rename table sorted by Project Name by default and eliminated the use of ::ng-deep in the Project Rename scss files. --- .../project-name-dialog.component.html | 2 + .../project-name-dialog.component.scss | 234 +++++++++--------- .../project-name-dialog.component.ts | 7 +- 3 files changed, 129 insertions(+), 114 deletions(-) diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.html b/src/app/components/shared/project-name-dialog/project-name-dialog.component.html index 810103917..2f278b3b3 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.html +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.html @@ -48,6 +48,8 @@

    mat-table [dataSource]="dataSource" matSort + matSortActive="name" + matSortDirection="asc" class="projects-table" > diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss b/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss index db5dd100a..6c531562f 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss @@ -1,75 +1,77 @@ -::ng-deep .mat-mdc-dialog-title { - padding-top: 24px !important; - - &::before { - display: none; +// Scoped to this dialog to prevent global style pollution +// since ViewEncapsulation.None is used +app-project-name-dialog { + .mat-mdc-dialog-title { + padding-top: 24px !important; + + &::before { + display: none; + } } -} -.dialog-title { - margin: 0; -} + .dialog-title { + margin: 0; + } -.dialog-subtitle { - margin: 4px 0 0 0; - opacity: 0.7; - font-size: 14px; + .dialog-subtitle { + margin: 4px 0 0 0; + opacity: 0.7; + font-size: 14px; - &.job-count-warning, - &.cannot-rename-warning { - color: #f44336; - opacity: 1; - font-weight: 500; + &.job-count-warning, + &.cannot-rename-warning { + color: #f44336; + opacity: 1; + font-weight: 500; + } } -} -.project-name-field { - width: 100%; + .project-name-field { + width: 100%; - ::ng-deep .mat-mdc-text-field-wrapper { - padding-top: 0; + .mat-mdc-text-field-wrapper { + padding-top: 0; + } } -} -.projects-label { - margin: 16px 0 8px 0; - opacity: 0.7; - font-size: 14px; + .projects-label { + margin: 16px 0 8px 0; + opacity: 0.7; + font-size: 14px; - &.bold { - font-weight: 500; - opacity: 1; + &.bold { + font-weight: 500; + opacity: 1; + } } -} -.projects-table-wrapper { - border: 1px solid rgba(0, 0, 0, 0.12); - border-radius: 4px; - margin-bottom: 8px; - overflow: hidden; + .projects-table-wrapper { + border: 1px solid rgba(0, 0, 0, 0.12); + border-radius: 4px; + margin-bottom: 8px; + overflow: hidden; - &.scrollable { - max-height: 220px; - overflow-y: auto; + &.scrollable { + max-height: 220px; + overflow-y: auto; + } } -} -.projects-table { - width: 100%; + .projects-table { + width: 100%; - .job-count-header, - .job-count-cell { - text-align: right; - width: 80px; + .job-count-header, + .job-count-cell { + text-align: right; + width: 80px; + } } -} -// Confirmation checkbox -.confirmation-checkbox { - display: block; - margin-top: 32px; + // Confirmation checkbox + .confirmation-checkbox { + display: block; + margin-top: 32px; - ::ng-deep { .mdc-form-field { align-items: flex-start; } @@ -83,80 +85,90 @@ } } } -} -// Progress bar styles -.progress-container { - text-align: center; - padding: 24px 0; + // Progress bar styles + .progress-container { + text-align: center; + padding: 24px 0; - mat-progress-bar { - margin-bottom: 16px; - } + mat-progress-bar { + margin-bottom: 16px; + } - .progress-text { - margin: 0; - font-size: 18px; - font-weight: 500; - } + .progress-text { + margin: 0; + font-size: 18px; + font-weight: 500; + } - .estimated-time { - margin: 8px 0 0 0; - font-size: 13px; - opacity: 0.7; + .estimated-time { + margin: 8px 0 0 0; + font-size: 13px; + opacity: 0.7; + } } -} -// Result styles -.result-container { - text-align: center; - padding: 16px 0; + // Result styles + .result-container { + text-align: center; + padding: 16px 0; - p { - margin: 0; - font-size: 14px; - } + p { + margin: 0; + font-size: 14px; + } - .result-success { - color: #4caf50; - } + .result-success { + color: #4caf50; + } - .result-warning { - color: #ff9800; - } + .result-warning { + color: #ff9800; + } - .result-error { - color: #f44336; + .result-error { + color: #f44336; + } } -} -// Failed projects list -.failed-projects { - margin-top: 16px; - text-align: left; + // Failed projects list + .failed-projects { + margin-top: 16px; + text-align: left; - .failed-projects-label { - font-size: 14px; - font-weight: 500; - color: #f44336; - margin-bottom: 8px; - } + .failed-projects-label { + font-size: 14px; + font-weight: 500; + color: #f44336; + margin-bottom: 8px; + } - .failed-projects-list { - margin: 0; - padding-left: 20px; - font-size: 13px; - border: 1px solid rgba(0, 0, 0, 0.12); - border-radius: 4px; - padding: 8px 8px 8px 28px; + .failed-projects-list { + margin: 0; + padding-left: 20px; + font-size: 13px; + border: 1px solid rgba(0, 0, 0, 0.12); + border-radius: 4px; + padding: 8px 8px 8px 28px; + + &.scrollable { + max-height: 120px; + overflow-y: auto; + } - &.scrollable { - max-height: 120px; - overflow-y: auto; + li { + padding: 2px 0; + } } + } +} - li { - padding: 2px 0; - } +// Remove underline from sort header - must be outside app-project-name-dialog +// scope because dialog content renders in CDK overlay. +// The underline appears on keyboard/program focused states. +.projects-table { + [mat-sort-header].cdk-keyboard-focused .mat-sort-header-container, + [mat-sort-header].cdk-program-focused .mat-sort-header-container { + border-bottom: none; } } diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts index 38e574f6b..a56966565 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts @@ -8,6 +8,7 @@ import { OnInit, signal, computed, + ViewEncapsulation, } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { @@ -35,7 +36,7 @@ import { MatRowDef, MatRow, } from '@angular/material/table'; -import { MatSort, MatSortHeader } from '@angular/material/sort'; +import { MatSort, MatSortModule } from '@angular/material/sort'; import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { SubSink } from 'subsink'; @@ -67,6 +68,7 @@ export interface ProjectNameDialogResult { templateUrl: './project-name-dialog.component.html', styleUrls: ['./project-name-dialog.component.scss'], standalone: true, + encapsulation: ViewEncapsulation.None, imports: [ FormsModule, MatDialogTitle, @@ -78,8 +80,7 @@ export interface ProjectNameDialogResult { MatProgressBarModule, MatCheckboxModule, MatTable, - MatSort, - MatSortHeader, + MatSortModule, MatColumnDef, MatHeaderCellDef, MatHeaderCell, From 0e4a9cc8c4ed92d57c0e7ab062e08cb98e24fadc Mon Sep 17 00:00:00 2001 From: Andrew Anderson Date: Fri, 19 Dec 2025 11:37:24 -0800 Subject: [PATCH 47/59] Conformity & Best Practices * Removed stale SCSS styles: Cleaned up .sortable, .sort-indicator, .projects-table-body-container, and custom table styling that was no longer needed after switching to Material table * Made dialog injects private: Changed dialogRef and data from public to private to match codebase pattern * Added readonly to displayedColumns: Added readonly modifier since the array never changes * Fixed track function: Changed track name to track $index to handle potential duplicate project names in the failed list --- .../project-name-dialog.component.html | 2 +- .../project-name-dialog.component.scss | 50 ++----------------- .../project-name-dialog.component.ts | 6 +-- 3 files changed, 7 insertions(+), 51 deletions(-) diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.html b/src/app/components/shared/project-name-dialog/project-name-dialog.component.html index c69b3e5f5..810103917 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.html +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.html @@ -182,7 +182,7 @@

    {{ 'RENAME_COMPLETE' | translate }}

    class="failed-projects-list" [class.scrollable]="failedProjectNames.length > 5" > - @for (name of failedProjectNames; track name) { + @for (name of failedProjectNames; track $index) {
  • {{ name }}
  • } diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss b/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss index e948ef3cc..db5dd100a 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss @@ -46,60 +46,16 @@ border: 1px solid rgba(0, 0, 0, 0.12); border-radius: 4px; margin-bottom: 8px; + overflow: hidden; &.scrollable { - .projects-table-body-container { - max-height: 185px; - overflow-y: scroll; - } - - > .projects-table .job-count-header { - padding-right: 29px; // 12px base + 17px scrollbar width - } + max-height: 220px; + overflow-y: auto; } } -.projects-table-body-container { - border-top: 1px solid rgba(0, 0, 0, 0.12); -} - .projects-table { width: 100%; - border-collapse: collapse; - - th, - td { - padding: 8px 12px; - font-size: 14px; - text-align: left; - } - - th { - font-weight: 500; - opacity: 0.7; - - &.sortable { - cursor: pointer; - user-select: none; - - &:hover { - opacity: 1; - } - } - } - - td { - border-top: 1px solid rgba(0, 0, 0, 0.12); - } - - tr:first-child td { - border-top: none; - } - - .sort-indicator { - font-size: 10px; - margin-left: 4px; - } .job-count-header, .job-count-cell { diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts index f92c1d20e..d773c8a59 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts @@ -98,8 +98,8 @@ export class ProjectNameDialogComponent @ViewChild('projectNameInput') projectNameInput: ElementRef; @ViewChild(MatSort) sort: MatSort; - dialogRef = inject>(MatDialogRef); - data = inject(MAT_DIALOG_DATA); + private dialogRef = inject>(MatDialogRef); + private data = inject(MAT_DIALOG_DATA); private hyp3Api = inject(Hyp3ApiService); // Use signals for reactive state @@ -112,7 +112,7 @@ export class ProjectNameDialogComponent // Material table with sorting public dataSource = new MatTableDataSource([]); - public displayedColumns: string[] = ['name', 'count']; + public readonly displayedColumns: string[] = ['name', 'count']; // Two-phase UI state public phase: DialogPhase = 'input'; From 27dd8ab81f32241e26c18c5aba11e1fee46355d2 Mon Sep 17 00:00:00 2001 From: Andrew Anderson Date: Fri, 19 Dec 2025 11:56:36 -0800 Subject: [PATCH 48/59] Made "(unnamed)" multilingual --- .../project-name-dialog/project-name-dialog.component.ts | 6 ++++-- src/assets/i18n/de.json | 1 + src/assets/i18n/en.json | 1 + src/assets/i18n/es.json | 1 + 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts index d773c8a59..38e574f6b 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts @@ -36,7 +36,7 @@ import { MatRow, } from '@angular/material/table'; import { MatSort, MatSortHeader } from '@angular/material/sort'; -import { TranslateModule } from '@ngx-translate/core'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { SubSink } from 'subsink'; import * as models from '@models'; @@ -101,6 +101,7 @@ export class ProjectNameDialogComponent private dialogRef = inject>(MatDialogRef); private data = inject(MAT_DIALOG_DATA); private hyp3Api = inject(Hyp3ApiService); + private translateService = inject(TranslateService); // Use signals for reactive state public projectName = signal(''); @@ -156,9 +157,10 @@ export class ProjectNameDialogComponent this.jobCount = products?.length; if (products) { + const unnamedLabel = this.translateService.instant('PROJECT_NAME_UNNAMED'); const counts = new Map(); products.forEach((product) => { - const name = product.metadata?.job?.name || '(unnamed)'; + const name = product.metadata?.job?.name || unnamedLabel; counts.set(name, (counts.get(name) || 0) + 1); }); diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json index 4ed88c97e..710f679de 100644 --- a/src/assets/i18n/de.json +++ b/src/assets/i18n/de.json @@ -567,6 +567,7 @@ "PROJECT_NAME": "Projektname", "PROJECT_NAME_MAX_LENGTH": "Projektname darf 100 Zeichen nicht überschreiten", "PROJECT_NAME_REQUIRED": "Projektname ist erforderlich", + "PROJECT_NAME_UNNAMED": "(unbenannt)", "PROJECT_NAME_UPDATE_ALL_TOOLTIP": "Projektname für alle geladenen Aufträge aktualisieren", "PROJECT_NAME_UPDATE_FOR_JOBS": "Projektname für {{count}} Aufträge aktualisieren", "PROJECTS_SELECTED": "Ausgewählte Projekte", diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 5df400064..b88ab00a2 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -678,6 +678,7 @@ "PROJECT_NAME": "Project Name", "PROJECT_NAME_MAX_LENGTH": "Project name cannot exceed 100 characters", "PROJECT_NAME_REQUIRED": "Project name is required", + "PROJECT_NAME_UNNAMED": "(unnamed)", "PROJECT_NAME_UPDATE_ALL_TOOLTIP": "Update project name for all loaded jobs", "PROJECT_NAME_UPDATE_FOR_JOBS": "Update project name for {{count}} jobs", "PROJECTS_SELECTED": "Projects Selected", diff --git a/src/assets/i18n/es.json b/src/assets/i18n/es.json index 12a74b900..ba4d9b069 100644 --- a/src/assets/i18n/es.json +++ b/src/assets/i18n/es.json @@ -679,6 +679,7 @@ "PROJECT_NAME": "Nombre del Proyecto", "PROJECT_NAME_MAX_LENGTH": "El nombre del proyecto no puede exceder 100 caracteres", "PROJECT_NAME_REQUIRED": "El nombre del proyecto es obligatorio", + "PROJECT_NAME_UNNAMED": "(sin nombre)", "PROJECT_NAME_UPDATE_ALL_TOOLTIP": "Actualizar nombre del proyecto para todos los trabajos cargados", "PROJECT_NAME_UPDATE_FOR_JOBS": "Actualizar nombre del proyecto para {{count}} trabajos", "PROJECTS_SELECTED": "Proyectos Seleccionados", From 9e84a38dd12c2f5eb87f37fba0f6f73f0100135a Mon Sep 17 00:00:00 2001 From: Andrew Anderson Date: Fri, 19 Dec 2025 12:30:03 -0800 Subject: [PATCH 49/59] Tweaks Made the Project Rename table sorted by Project Name by default and eliminated the use of ::ng-deep in the Project Rename scss files. --- .../project-name-dialog.component.html | 2 + .../project-name-dialog.component.scss | 234 +++++++++--------- .../project-name-dialog.component.ts | 7 +- 3 files changed, 129 insertions(+), 114 deletions(-) diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.html b/src/app/components/shared/project-name-dialog/project-name-dialog.component.html index 810103917..2f278b3b3 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.html +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.html @@ -48,6 +48,8 @@

    mat-table [dataSource]="dataSource" matSort + matSortActive="name" + matSortDirection="asc" class="projects-table" > diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss b/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss index db5dd100a..6c531562f 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss @@ -1,75 +1,77 @@ -::ng-deep .mat-mdc-dialog-title { - padding-top: 24px !important; - - &::before { - display: none; +// Scoped to this dialog to prevent global style pollution +// since ViewEncapsulation.None is used +app-project-name-dialog { + .mat-mdc-dialog-title { + padding-top: 24px !important; + + &::before { + display: none; + } } -} -.dialog-title { - margin: 0; -} + .dialog-title { + margin: 0; + } -.dialog-subtitle { - margin: 4px 0 0 0; - opacity: 0.7; - font-size: 14px; + .dialog-subtitle { + margin: 4px 0 0 0; + opacity: 0.7; + font-size: 14px; - &.job-count-warning, - &.cannot-rename-warning { - color: #f44336; - opacity: 1; - font-weight: 500; + &.job-count-warning, + &.cannot-rename-warning { + color: #f44336; + opacity: 1; + font-weight: 500; + } } -} -.project-name-field { - width: 100%; + .project-name-field { + width: 100%; - ::ng-deep .mat-mdc-text-field-wrapper { - padding-top: 0; + .mat-mdc-text-field-wrapper { + padding-top: 0; + } } -} -.projects-label { - margin: 16px 0 8px 0; - opacity: 0.7; - font-size: 14px; + .projects-label { + margin: 16px 0 8px 0; + opacity: 0.7; + font-size: 14px; - &.bold { - font-weight: 500; - opacity: 1; + &.bold { + font-weight: 500; + opacity: 1; + } } -} -.projects-table-wrapper { - border: 1px solid rgba(0, 0, 0, 0.12); - border-radius: 4px; - margin-bottom: 8px; - overflow: hidden; + .projects-table-wrapper { + border: 1px solid rgba(0, 0, 0, 0.12); + border-radius: 4px; + margin-bottom: 8px; + overflow: hidden; - &.scrollable { - max-height: 220px; - overflow-y: auto; + &.scrollable { + max-height: 220px; + overflow-y: auto; + } } -} -.projects-table { - width: 100%; + .projects-table { + width: 100%; - .job-count-header, - .job-count-cell { - text-align: right; - width: 80px; + .job-count-header, + .job-count-cell { + text-align: right; + width: 80px; + } } -} -// Confirmation checkbox -.confirmation-checkbox { - display: block; - margin-top: 32px; + // Confirmation checkbox + .confirmation-checkbox { + display: block; + margin-top: 32px; - ::ng-deep { .mdc-form-field { align-items: flex-start; } @@ -83,80 +85,90 @@ } } } -} -// Progress bar styles -.progress-container { - text-align: center; - padding: 24px 0; + // Progress bar styles + .progress-container { + text-align: center; + padding: 24px 0; - mat-progress-bar { - margin-bottom: 16px; - } + mat-progress-bar { + margin-bottom: 16px; + } - .progress-text { - margin: 0; - font-size: 18px; - font-weight: 500; - } + .progress-text { + margin: 0; + font-size: 18px; + font-weight: 500; + } - .estimated-time { - margin: 8px 0 0 0; - font-size: 13px; - opacity: 0.7; + .estimated-time { + margin: 8px 0 0 0; + font-size: 13px; + opacity: 0.7; + } } -} -// Result styles -.result-container { - text-align: center; - padding: 16px 0; + // Result styles + .result-container { + text-align: center; + padding: 16px 0; - p { - margin: 0; - font-size: 14px; - } + p { + margin: 0; + font-size: 14px; + } - .result-success { - color: #4caf50; - } + .result-success { + color: #4caf50; + } - .result-warning { - color: #ff9800; - } + .result-warning { + color: #ff9800; + } - .result-error { - color: #f44336; + .result-error { + color: #f44336; + } } -} -// Failed projects list -.failed-projects { - margin-top: 16px; - text-align: left; + // Failed projects list + .failed-projects { + margin-top: 16px; + text-align: left; - .failed-projects-label { - font-size: 14px; - font-weight: 500; - color: #f44336; - margin-bottom: 8px; - } + .failed-projects-label { + font-size: 14px; + font-weight: 500; + color: #f44336; + margin-bottom: 8px; + } - .failed-projects-list { - margin: 0; - padding-left: 20px; - font-size: 13px; - border: 1px solid rgba(0, 0, 0, 0.12); - border-radius: 4px; - padding: 8px 8px 8px 28px; + .failed-projects-list { + margin: 0; + padding-left: 20px; + font-size: 13px; + border: 1px solid rgba(0, 0, 0, 0.12); + border-radius: 4px; + padding: 8px 8px 8px 28px; + + &.scrollable { + max-height: 120px; + overflow-y: auto; + } - &.scrollable { - max-height: 120px; - overflow-y: auto; + li { + padding: 2px 0; + } } + } +} - li { - padding: 2px 0; - } +// Remove underline from sort header - must be outside app-project-name-dialog +// scope because dialog content renders in CDK overlay. +// The underline appears on keyboard/program focused states. +.projects-table { + [mat-sort-header].cdk-keyboard-focused .mat-sort-header-container, + [mat-sort-header].cdk-program-focused .mat-sort-header-container { + border-bottom: none; } } diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts index 38e574f6b..a56966565 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts @@ -8,6 +8,7 @@ import { OnInit, signal, computed, + ViewEncapsulation, } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { @@ -35,7 +36,7 @@ import { MatRowDef, MatRow, } from '@angular/material/table'; -import { MatSort, MatSortHeader } from '@angular/material/sort'; +import { MatSort, MatSortModule } from '@angular/material/sort'; import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { SubSink } from 'subsink'; @@ -67,6 +68,7 @@ export interface ProjectNameDialogResult { templateUrl: './project-name-dialog.component.html', styleUrls: ['./project-name-dialog.component.scss'], standalone: true, + encapsulation: ViewEncapsulation.None, imports: [ FormsModule, MatDialogTitle, @@ -78,8 +80,7 @@ export interface ProjectNameDialogResult { MatProgressBarModule, MatCheckboxModule, MatTable, - MatSort, - MatSortHeader, + MatSortModule, MatColumnDef, MatHeaderCellDef, MatHeaderCell, From 77ae12f6e51ebf81348664b7eec051b2afbc8080 Mon Sep 17 00:00:00 2001 From: Andrew Anderson Date: Fri, 19 Dec 2025 12:48:23 -0800 Subject: [PATCH 50/59] npm run lint -- --fix --- .../project-name-dialog/project-name-dialog.component.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts index a56966565..b1228ce35 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts @@ -99,7 +99,8 @@ export class ProjectNameDialogComponent @ViewChild('projectNameInput') projectNameInput: ElementRef; @ViewChild(MatSort) sort: MatSort; - private dialogRef = inject>(MatDialogRef); + private dialogRef = + inject>(MatDialogRef); private data = inject(MAT_DIALOG_DATA); private hyp3Api = inject(Hyp3ApiService); private translateService = inject(TranslateService); @@ -158,7 +159,9 @@ export class ProjectNameDialogComponent this.jobCount = products?.length; if (products) { - const unnamedLabel = this.translateService.instant('PROJECT_NAME_UNNAMED'); + const unnamedLabel = this.translateService.instant( + 'PROJECT_NAME_UNNAMED', + ); const counts = new Map(); products.forEach((product) => { const name = product.metadata?.job?.name || unnamedLabel; From 5d76b1e028cafdb95d9054159845b8cf6b62628f Mon Sep 17 00:00:00 2001 From: Andrew Anderson Date: Fri, 19 Dec 2025 13:03:34 -0800 Subject: [PATCH 51/59] Made the table header 'sticky' --- .../project-name-dialog.component.scss | 8 ++++++++ .../project-name-dialog/project-name-dialog.component.ts | 7 +++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss b/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss index 6c531562f..8ff3a4952 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss @@ -60,6 +60,14 @@ app-project-name-dialog { .projects-table { width: 100%; + // Sticky header so it stays visible while scrolling + th.mat-mdc-header-cell { + position: sticky; + top: 0; + z-index: 1; + background-color: white; + } + .job-count-header, .job-count-cell { text-align: right; diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts index a56966565..b1228ce35 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts @@ -99,7 +99,8 @@ export class ProjectNameDialogComponent @ViewChild('projectNameInput') projectNameInput: ElementRef; @ViewChild(MatSort) sort: MatSort; - private dialogRef = inject>(MatDialogRef); + private dialogRef = + inject>(MatDialogRef); private data = inject(MAT_DIALOG_DATA); private hyp3Api = inject(Hyp3ApiService); private translateService = inject(TranslateService); @@ -158,7 +159,9 @@ export class ProjectNameDialogComponent this.jobCount = products?.length; if (products) { - const unnamedLabel = this.translateService.instant('PROJECT_NAME_UNNAMED'); + const unnamedLabel = this.translateService.instant( + 'PROJECT_NAME_UNNAMED', + ); const counts = new Map(); products.forEach((product) => { const name = product.metadata?.job?.name || unnamedLabel; From 67ff1b41a3168bfdb5de6617ffa2418378dc36c0 Mon Sep 17 00:00:00 2001 From: Andrew Anderson Date: Fri, 19 Dec 2025 13:29:06 -0800 Subject: [PATCH 52/59] Fixed Table Header for Dark-Theme --- .../project-name-dialog/project-name-dialog.component.scss | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss b/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss index 8ff3a4952..946a24eb5 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss @@ -65,7 +65,13 @@ app-project-name-dialog { position: sticky; top: 0; z-index: 1; + // Default light theme background background-color: white; + + // Dark theme background + .theme-dark & { + background-color: #424242; + } } .job-count-header, From f4380e5ea585d6d139f3ce143a9b85b5c9d7ff05 Mon Sep 17 00:00:00 2001 From: Andrew Anderson Date: Fri, 19 Dec 2025 13:50:59 -0800 Subject: [PATCH 53/59] Show estimated time for 5+ jobs It was 1000+ before but Tools wants to see it always. --- .../project-name-dialog/project-name-dialog.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts index b1228ce35..7f185651c 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts @@ -129,8 +129,8 @@ export class ProjectNameDialogComponent public isValid = computed(() => this.projectName().trim().length > 0); public formattedTimeRemaining = computed(() => { - // Only show estimated time for large operations (1000+ jobs) - if (this.jobCount < 1000 || this.estimatedSecondsRemaining() === null) { + // Only show estimated time for operations of 5+ jobs + if (this.jobCount < 5 || this.estimatedSecondsRemaining() === null) { return null; } From eb391034daa665a44e5e98f35322e65c7825e5db Mon Sep 17 00:00:00 2001 From: Andrew Anderson Date: Fri, 19 Dec 2025 14:01:40 -0800 Subject: [PATCH 54/59] Warn about Abort I added a @HostListener for the window:beforeunload event that triggers only when the dialog is in the processing phase. This will: - Show the browser's native "Leave site?" confirmation dialog when the user tries to close the tab/window or navigate away - Only trigger during the processing phase - users can freely close during input, complete, or error phases - Let the browser handle the UI (modern browsers don't allow custom messages for security reasons) --- .../project-name-dialog.component.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts index 7f185651c..f0fd90c57 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts @@ -9,6 +9,7 @@ import { signal, computed, ViewEncapsulation, + HostListener, } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { @@ -256,6 +257,15 @@ export class ProjectNameDialogComponent this.dialogRef.close(result); } + @HostListener('window:beforeunload', ['$event']) + onBeforeUnload(event: BeforeUnloadEvent): void { + if (this.phase === 'processing') { + event.preventDefault(); + // Modern browsers ignore custom messages, but returnValue is still required + event.returnValue = ''; + } + } + ngOnDestroy(): void { this.subs.unsubscribe(); } From 501a17a384355896300e6aec46efe67aaf8d64b7 Mon Sep 17 00:00:00 2001 From: William Horn Date: Fri, 19 Dec 2025 13:53:24 -0900 Subject: [PATCH 55/59] Add toggle for removing project names in update dialog --- .../project-name-dialog.component.html | 65 +++++++++++++------ .../project-name-dialog.component.scss | 5 ++ .../project-name-dialog.component.ts | 11 +++- src/assets/i18n/en.json | 4 +- src/assets/i18n/es.json | 4 +- 5 files changed, 66 insertions(+), 23 deletions(-) diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.html b/src/app/components/shared/project-name-dialog/project-name-dialog.component.html index 2f278b3b3..884fceaf9 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.html +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.html @@ -15,26 +15,39 @@

    + +
    + + Edit + Remove + +
    + - - {{ 'PROJECT_NAME' | translate }} - - {{ projectName()?.length || 0 }}/100 - @if (!projectName()?.trim()) { - {{ 'PROJECT_NAME_REQUIRED' | translate }} - } @else if (projectName()?.length >= 100) { - {{ 'PROJECT_NAME_MAX_LENGTH' | translate }} - } - + @if (isEditMode()) { + + {{ 'PROJECT_NAME' | translate }} + + {{ projectName()?.length || 0 }}/100 + @if (!projectName()?.trim()) { + {{ 'PROJECT_NAME_REQUIRED' | translate }} + } @else if (projectName()?.length >= 100) { + {{ 'PROJECT_NAME_MAX_LENGTH' | translate }} + } + + } @if (dataSource.data.length > 0) {

    @@ -86,6 +99,7 @@

    [(ngModel)]="confirmationChecked" name="confirmationChecked" > + @if (isEditMode()) { {{ (projectCount === 1 ? 'CONFIRM_RENAME_SINGLE' @@ -98,6 +112,19 @@

    newName: projectName()?.trim(), } }} + } @else { + {{ + (projectCount === 1 + ? 'CONFIRM_REMOVE_SINGLE' + : 'CONFIRM_REMOVE_MULTIPLE' + ) + | translate + : { + projectCount: projectCount, + jobCount: jobCount, + } + }} + } } } diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss b/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss index 946a24eb5..a2fd056ed 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss @@ -1,6 +1,7 @@ // Scoped to this dialog to prevent global style pollution // since ViewEncapsulation.None is used app-project-name-dialog { + .mat-mdc-dialog-title { padding-top: 24px !important; @@ -26,6 +27,10 @@ app-project-name-dialog { } } + .selection-type-toggle { + margin: 8px 16px; + } + .project-name-field { width: 100%; diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts index f0fd90c57..657de4c5b 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts @@ -37,6 +37,7 @@ import { MatRowDef, MatRow, } from '@angular/material/table'; +import { MatButtonToggleModule } from '@angular/material/button-toggle'; import { MatSort, MatSortModule } from '@angular/material/sort'; import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { SubSink } from 'subsink'; @@ -92,6 +93,7 @@ export interface ProjectNameDialogResult { MatRowDef, MatRow, TranslateModule, + MatButtonToggleModule, ], }) export class ProjectNameDialogComponent @@ -125,9 +127,14 @@ export class ProjectNameDialogComponent public successCount = 0; public failedCount = 0; public failedProjectNames: string[] = []; + public projectEditType = signal<'edit' | 'remove'>('edit'); // Computed values (only recalculate when dependencies change) - public isValid = computed(() => this.projectName().trim().length > 0); + public isEditMode = computed(() => this.projectEditType() === 'edit'); + public isRemoveMode = computed(() => this.projectEditType() === 'remove'); + public isValid = computed( + () => this.projectName().trim().length > 0 || this.isRemoveMode(), + ); public formattedTimeRemaining = computed(() => { // Only show estimated time for operations of 5+ jobs @@ -204,7 +211,7 @@ export class ProjectNameDialogComponent return; } - const trimmedName = this.projectName().trim(); + const trimmedName = this.isEditMode() ? this.projectName().trim() : null; // If no products, just return the name (single-file rename flow) if (!this.data.products || this.data.products.length === 0) { diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index b88ab00a2..ab14adcfe 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -126,6 +126,8 @@ "CANNOT_RENAME_PROJECTS_FOR": "You cannot rename projects for", "CONFIRM_RENAME_MULTIPLE": "Yes! I really want to rename these {{projectCount}} projects ({{jobCount}} jobs) to \"{{newName}}\".", "CONFIRM_RENAME_SINGLE": "Yes! I really want to rename this 1 project ({{jobCount}} jobs) to \"{{newName}}\".", + "CONFIRM_REMOVE_MULTIPLE": "Yes! I really want to remove the names from these {{projectCount}} projects ({{jobCount}} jobs).", + "CONFIRM_REMOVE_SINGLE": "Yes! I really want to remove the project name from this project ({{jobCount}} jobs).", "CENTER_COLUMN_AND_FILES_COLUMN_RIGHT_WILL_POPULATE": "(center column) and Files column (right) will populate.", "CHARACTERS": "characters", "CHART": "Chart", @@ -1050,4 +1052,4 @@ "ZOOM_TO_FIT": "Zoom To Fit", "ZOOM_TO_RESULTS": "Zoom to results", "ZOOM_TO_SCENE": "Zoom to scene" -} \ No newline at end of file +} diff --git a/src/assets/i18n/es.json b/src/assets/i18n/es.json index ba4d9b069..aed11e96a 100644 --- a/src/assets/i18n/es.json +++ b/src/assets/i18n/es.json @@ -126,6 +126,8 @@ "CANNOT_RENAME_PROJECTS_FOR": "No puede cambiar el nombre de los proyectos de", "CONFIRM_RENAME_MULTIPLE": "¡Sí! Realmente quiero renombrar estos {{projectCount}} proyectos ({{jobCount}} trabajos) a \"{{newName}}\".", "CONFIRM_RENAME_SINGLE": "¡Sí! Realmente quiero renombrar este 1 proyecto ({{jobCount}} trabajos) a \"{{newName}}\".", + "CONFIRM_REMOVE_MULTIPLE": "TODO", + "CONFIRM_REMOVE_SINGLE": "TODO", "CENTER_COLUMN_AND_FILES_COLUMN_RIGHT_WILL_POPULATE": "(columna central) y la columna Archivos (derecha) se completarán.", "CHARACTERS": "caracteres", "CHART": "Gráfico", @@ -1051,4 +1053,4 @@ "ZOOM_TO_FIT": "Acercar para ajustar", "ZOOM_TO_RESULTS": "Acercar a los resultados", "ZOOM_TO_SCENE": "Acercar a la escena" -} \ No newline at end of file +} From 721dba414de711553de300d5dd2a1ac6824e341d Mon Sep 17 00:00:00 2001 From: Andrew Anderson Date: Mon, 5 Jan 2026 18:03:39 -0800 Subject: [PATCH 56/59] feat(project-name-dialog): replace button toggle with radio buttons - Replace mat-button-toggle-group with mat-radio-group for cleaner UX - Input field now visually nested under "Rename" radio option - Add singular/plural translation keys for radio labels (en, es, de) - Add accessible aria-label to radio group - Update styles for radio group layout and disabled state - Add docs/ to .gitignore --- .gitignore | 1 + .../project-name-dialog.component.html | 94 +++++++++++-------- .../project-name-dialog.component.scss | 24 ++++- .../project-name-dialog.component.ts | 4 +- src/assets/i18n/de.json | 4 + src/assets/i18n/en.json | 4 + src/assets/i18n/es.json | 4 + 7 files changed, 90 insertions(+), 45 deletions(-) diff --git a/.gitignore b/.gitignore index d8cb675af..bbb61b064 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,4 @@ local-serve.sh .claude/ CLAUDE.md TRANSLATIONS_SCRATCH.md +docs/ diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.html b/src/app/components/shared/project-name-dialog/project-name-dialog.component.html index 884fceaf9..ad685235b 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.html +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.html @@ -15,20 +15,26 @@

    - -
    - + - Edit - Remove - -
    + class="project-edit-radio-group" + aria-label="Project edit action" + > + + @if (jobCount <= 1) { + {{ 'RENAME_JOB_TO' | translate }} + } @else { + {{ 'RENAME_JOBS_TO' | translate: { count: jobCount } }} + } + - - @if (isEditMode()) { - + {{ 'PROJECT_NAME' | translate }} name="projectName" maxlength="100" required - [disabled]="isDisabledByUserFilter" + [disabled]="isDisabledByUserFilter || isRemoveMode()" /> {{ projectName()?.length || 0 }}/100 @if (!projectName()?.trim()) { @@ -47,7 +53,15 @@

    {{ 'PROJECT_NAME_MAX_LENGTH' | translate }} } - } + + + @if (projectCount <= 1) { + {{ 'REMOVE_PROJECT_NAME' | translate }} + } @else { + {{ 'REMOVE_PROJECT_NAMES' | translate: { count: projectCount } }} + } + + @if (dataSource.data.length > 0) {

    @@ -99,32 +113,32 @@

    [(ngModel)]="confirmationChecked" name="confirmationChecked" > - @if (isEditMode()) { - {{ - (projectCount === 1 - ? 'CONFIRM_RENAME_SINGLE' - : 'CONFIRM_RENAME_MULTIPLE' - ) - | translate - : { - projectCount: projectCount, - jobCount: jobCount, - newName: projectName()?.trim(), - } - }} - } @else { - {{ - (projectCount === 1 - ? 'CONFIRM_REMOVE_SINGLE' - : 'CONFIRM_REMOVE_MULTIPLE' - ) - | translate - : { - projectCount: projectCount, - jobCount: jobCount, - } - }} - } + @if (isEditMode()) { + {{ + (projectCount === 1 + ? 'CONFIRM_RENAME_SINGLE' + : 'CONFIRM_RENAME_MULTIPLE' + ) + | translate + : { + projectCount: projectCount, + jobCount: jobCount, + newName: projectName()?.trim(), + } + }} + } @else { + {{ + (projectCount === 1 + ? 'CONFIRM_REMOVE_SINGLE' + : 'CONFIRM_REMOVE_MULTIPLE' + ) + | translate + : { + projectCount: projectCount, + jobCount: jobCount, + } + }} + } } } diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss b/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss index a2fd056ed..204b83569 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss @@ -27,16 +27,34 @@ app-project-name-dialog { } } - .selection-type-toggle { - margin: 8px 16px; + // Radio group layout for edit/remove toggle + .project-edit-radio-group { + display: flex; + flex-direction: column; + gap: 8px; + margin-bottom: 16px; } .project-name-field { - width: 100%; + margin-left: 32px; + width: calc(100% - 32px); .mat-mdc-text-field-wrapper { padding-top: 0; } + + // Subtle disabled state when "Remove" is selected + &.disabled { + opacity: 0.5; + pointer-events: none; + } + } + + // Radio button label sizing + .mat-mdc-radio-button { + .mdc-label { + font-size: 14px; + } } .projects-label { diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts index 657de4c5b..0ddf2efbd 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts @@ -37,7 +37,7 @@ import { MatRowDef, MatRow, } from '@angular/material/table'; -import { MatButtonToggleModule } from '@angular/material/button-toggle'; +import { MatRadioModule } from '@angular/material/radio'; import { MatSort, MatSortModule } from '@angular/material/sort'; import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { SubSink } from 'subsink'; @@ -93,7 +93,7 @@ export interface ProjectNameDialogResult { MatRowDef, MatRow, TranslateModule, - MatButtonToggleModule, + MatRadioModule, ], }) export class ProjectNameDialogComponent diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json index 710f679de..2849fc25a 100644 --- a/src/assets/i18n/de.json +++ b/src/assets/i18n/de.json @@ -597,6 +597,8 @@ "REMOVE_FILE_FROM_DOWNLOADS": "Datei aus Downloads entfernen", "REMOVE_FILE_FROM_QUEUE": "Datei aus der Warteschlange entfernen", "REMOVE_JOB": "Job entfernen", + "REMOVE_PROJECT_NAME": "Projektname entfernen", + "REMOVE_PROJECT_NAMES": "{{count}} Projektnamen entfernen", "REMOVE_RED_EYE": "remove_red_eye", "REMOVE_SCENE_FILES_FROM_DOWNLOADS": "Entfernen von Szenendateien aus Downloads", "RESAMPLED_DEM_SRTM_OR_NED_USED_FOR_RTC_PROCESSING": "Resampled DEM (SRTM oder NED), das für die RTC-Verarbeitung verwendet wird.", @@ -609,6 +611,8 @@ "RENAME_ALL_FAILED": "{{count}} Auftrag/Aufträge konnten nicht umbenannt werden.", "RENAME_COMPLETE": "Umbenennung abgeschlossen", "RENAME_ERROR": "Beim Umbenennen der Aufträge ist ein Fehler aufgetreten.", + "RENAME_JOB_TO": "Job umbenennen zu:", + "RENAME_JOBS_TO": "{{count}} Jobs umbenennen zu:", "RENAME_PARTIAL_SUCCESS": "{{success}} Auftrag/Aufträge umbenannt. {{failed}} fehlgeschlagen.", "RENAME_SUCCESS": "{{count}} Auftrag/Aufträge erfolgreich umbenannt.", "RENAMING_JOBS": "Aufträge werden umbenannt...", diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index ab14adcfe..8e5f2fa6a 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -715,6 +715,8 @@ "REMOVE_FILE_FROM_DOWNLOADS": "Remove file from downloads", "REMOVE_FILE_FROM_QUEUE": "Remove file from queue", "REMOVE_JOB": "Remove job", + "REMOVE_PROJECT_NAME": "Remove project name", + "REMOVE_PROJECT_NAMES": "Remove {{count}} project names", "REMOVE_RED_EYE": "remove_red_eye", "REMOVE_SCENE_FILES_FROM_DOWNLOADS": "Remove scene files from downloads", "REMOVE_SERIES": "Remove Series", @@ -730,6 +732,8 @@ "RENAME_ALL_FAILED": "Failed to rename {{count}} job(s).", "RENAME_COMPLETE": "Rename Complete", "RENAME_ERROR": "An error occurred while renaming jobs.", + "RENAME_JOB_TO": "Rename job to:", + "RENAME_JOBS_TO": "Rename {{count}} jobs to:", "RENAME_PARTIAL_SUCCESS": "Renamed {{success}} job(s). {{failed}} failed.", "RENAME_SUCCESS": "Successfully renamed {{count}} job(s).", "RENAMING_JOBS": "Renaming jobs...", diff --git a/src/assets/i18n/es.json b/src/assets/i18n/es.json index aed11e96a..77ae1eda1 100644 --- a/src/assets/i18n/es.json +++ b/src/assets/i18n/es.json @@ -716,6 +716,8 @@ "REMOVE_FILE_FROM_DOWNLOADS": "Eliminar archivo de las descargas", "REMOVE_FILE_FROM_QUEUE": "Eliminar archivo de la lista", "REMOVE_JOB": "Quitar trabajo", + "REMOVE_PROJECT_NAME": "Eliminar nombre del proyecto", + "REMOVE_PROJECT_NAMES": "Eliminar {{count}} nombres de proyecto", "REMOVE_RED_EYE": "quitar_ojo_rojo", "REMOVE_SCENE_FILES_FROM_DOWNLOADS": "Eliminar archivos de escena de las descargas", "REMOVE_SERIES": "Eliminar serie", @@ -731,6 +733,8 @@ "RENAME_ALL_FAILED": "Error al renombrar {{count}} trabajo(s).", "RENAME_COMPLETE": "Cambio de nombre completado", "RENAME_ERROR": "Ocurrió un error al renombrar los trabajos.", + "RENAME_JOB_TO": "Renombrar trabajo a:", + "RENAME_JOBS_TO": "Renombrar {{count}} trabajos a:", "RENAME_PARTIAL_SUCCESS": "Se renombraron {{success}} trabajo(s). {{failed}} fallaron.", "RENAME_SUCCESS": "Se renombraron {{count}} trabajo(s) exitosamente.", "RENAMING_JOBS": "Renombrando trabajos...", From 41fe0372b4d0f66e2773824a80d0ae03d3858ff3 Mon Sep 17 00:00:00 2001 From: Andrew Anderson Date: Mon, 5 Jan 2026 19:06:14 -0800 Subject: [PATCH 57/59] Update src/assets/i18n/es.json Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/assets/i18n/es.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/assets/i18n/es.json b/src/assets/i18n/es.json index 77ae1eda1..2f32c1015 100644 --- a/src/assets/i18n/es.json +++ b/src/assets/i18n/es.json @@ -126,8 +126,8 @@ "CANNOT_RENAME_PROJECTS_FOR": "No puede cambiar el nombre de los proyectos de", "CONFIRM_RENAME_MULTIPLE": "¡Sí! Realmente quiero renombrar estos {{projectCount}} proyectos ({{jobCount}} trabajos) a \"{{newName}}\".", "CONFIRM_RENAME_SINGLE": "¡Sí! Realmente quiero renombrar este 1 proyecto ({{jobCount}} trabajos) a \"{{newName}}\".", - "CONFIRM_REMOVE_MULTIPLE": "TODO", - "CONFIRM_REMOVE_SINGLE": "TODO", + "CONFIRM_REMOVE_MULTIPLE": "¡Sí! Realmente quiero eliminar estos {{projectCount}} proyectos ({{jobCount}} trabajos).", + "CONFIRM_REMOVE_SINGLE": "¡Sí! Realmente quiero eliminar este 1 proyecto ({{jobCount}} trabajos).", "CENTER_COLUMN_AND_FILES_COLUMN_RIGHT_WILL_POPULATE": "(columna central) y la columna Archivos (derecha) se completarán.", "CHARACTERS": "caracteres", "CHART": "Gráfico", From 9606e426d5a5d7ad3577c9f17986240565b12bad Mon Sep 17 00:00:00 2001 From: Andrew Anderson Date: Mon, 5 Jan 2026 19:06:44 -0800 Subject: [PATCH 58/59] Update src/app/components/shared/project-name-dialog/project-name-dialog.component.scss Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../project-name-dialog/project-name-dialog.component.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss b/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss index 204b83569..fa95893eb 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.scss @@ -1,7 +1,6 @@ // Scoped to this dialog to prevent global style pollution // since ViewEncapsulation.None is used app-project-name-dialog { - .mat-mdc-dialog-title { padding-top: 24px !important; From 37afaf1eba965cdc153a92273eb6e27b970b4da2 Mon Sep 17 00:00:00 2001 From: Andrew Anderson Date: Mon, 5 Jan 2026 19:25:19 -0800 Subject: [PATCH 59/59] Bug Fix A single job rename didn't reflect the count of '1' in the dialog for renaming a job. It now does. --- .../shared/project-name-dialog/project-name-dialog.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts index 0ddf2efbd..d5b5e5a10 100644 --- a/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts +++ b/src/app/components/shared/project-name-dialog/project-name-dialog.component.ts @@ -164,7 +164,7 @@ export class ProjectNameDialogComponent } const products = this.data?.products; - this.jobCount = products?.length; + this.jobCount = products?.length ?? 1; if (products) { const unnamedLabel = this.translateService.instant(