From 316661114810c400bc737048354e60f50c162c42 Mon Sep 17 00:00:00 2001 From: Daniel Truong Date: Wed, 14 Oct 2026 17:47:29 +0000 Subject: [PATCH 1/3] refactor(search): read search tabs from DEMI - Documents and Projects tabs read /documents and /projects with q, page, pageSize, count and native filter names. Only columns on the DEMI sort allow-list sort. The comment period filter is gone. - fullProjectResolver keeps its eagle-api read: DEMI rows hold only the current legislation version. - Drop the metrics page (eagle-api /audit has no controller), SearchService, OrgService.getByCompanyType and ApiService.head. --- src/app/app-routing.module.ts | 8 +- src/app/metrics/metrics.component.css | 206 ------ src/app/metrics/metrics.component.html | 29 - src/app/metrics/metrics.component.ts | 13 - .../resolvers/full-project.resolver.spec.ts | 75 ++ src/app/resolvers/full-project.resolver.ts | 35 +- .../search-document-table-rows.component.html | 4 +- ...arch-document-table-rows.component.spec.ts | 26 + .../search-document-table-rows.component.ts | 12 +- src/app/search/search.component.css | 12 - src/app/search/search.component.html | 31 +- src/app/search/search.component.spec.ts | 307 +++++++- src/app/search/search.component.ts | 688 ++++++++---------- src/app/services/api.spec.ts | 9 +- src/app/services/api.ts | 4 - src/app/services/org.service.ts | 19 +- src/app/services/search.service.ts | 105 --- 17 files changed, 720 insertions(+), 863 deletions(-) delete mode 100644 src/app/metrics/metrics.component.css delete mode 100644 src/app/metrics/metrics.component.html delete mode 100644 src/app/metrics/metrics.component.ts create mode 100644 src/app/resolvers/full-project.resolver.spec.ts create mode 100644 src/app/search/search-document-table-rows/search-document-table-rows.component.spec.ts delete mode 100644 src/app/services/search.service.ts diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index d412313c0..dc32f12b4 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -36,7 +36,6 @@ import { GroupContactSelectComponent } from './project/project-groups/group-cont import { LinkOrganizationComponent } from './shared/components/link-organization/link-organization.component'; import { LoginComponent } from './login/login.component'; import { MapComponent } from './map/map.component'; -import { MetricsComponent } from './metrics/metrics.component'; import { MilestonesComponent } from './project/milestones/milestones.component'; import { NotAuthorizedComponent } from './not-authorized/not-authorized.component'; import { OrganizationsComponent } from './organizations/organizations.component'; @@ -114,17 +113,12 @@ export const routes: Routes = [ canActivate: [AuthGuard], }, - // Map & Metrics & Search + // Map & Search { path: 'map', component: MapComponent, canActivate: [AuthGuard] }, - { - path: 'metrics', - component: MetricsComponent, - canActivate: [AuthGuard] - }, { path: 'search', component: SearchComponent, diff --git a/src/app/metrics/metrics.component.css b/src/app/metrics/metrics.component.css deleted file mode 100644 index 50f9a2c8f..000000000 --- a/src/app/metrics/metrics.component.css +++ /dev/null @@ -1,206 +0,0 @@ -/* Move somewhere global */ -h1 { - letter-spacing: -0.05rem; -} - -h2 { - font-size: 1.5rem; - font-weight: 400; - letter-spacing: -0.03rem; -} - -h2 strong { - font-weight: 400; -} - -.btn-sm i { - margin-top: -0.1rem; -} - -/* Search Container */ -.search-container { - padding-top: 1.5rem; - padding-bottom: .5rem; - background: #f7f8fa; -} - -form { - margin-top: 2rem; -} - -.main-search-field input { - height: 3rem; - color: #000; - font-size: 1.15rem; -} - -.main-search-field input:-webkit-autofill { - box-shadow: 0 0 0 30px white inset; -} - -.main-search-field button { - min-width: 10rem; - font-weight: 700; -} - -.main-search-field .input-group-addon { - font-size: 1.15rem; -} - -.form-text { - margin-top: 0.55rem; - font-size: 0.85rem; -} - -/* CSS Variables */ -:root { - --sr-table-row-bg: #f7f8fa; - --cl-arrow-size: 3.0rem; - --cl-arrow-color: #036; - --cl-arrow-font-color: #fff; -} - -/* Search Results Table */ -.search-results table { - margin-top: .5rem; - table-layout: fixed; -} - -.search-results th { - padding: 0.5rem 1rem; - overflow: hidden; - border: none; - background: var(--sr-table-row-bg); - text-overflow: ellipsis; - font-size: 0.875rem; -} - -.search-results td { - padding: 1.25rem 1rem; - border: none; - background: var(--sr-table-row-bg); - line-height: 1.15rem; - font-size: 0.9375rem; -} - -/* Search Table Columns */ -.cl-file { - padding-right: 0; - width: 7rem; - overflow: hidden; -} - -/* Crown Land File Number Arrow */ -.cl-arrow { - position: relative; - display: inline-block; - height: var(--cl-arrow-size); - padding-top: 0.55rem; - padding-right: 0.65rem; - padding-left: 1rem; - color: var(--cl-arrow-font-color); - background: var(--cl-arrow-color); - font-weight: bold; -} - -.cl-file .cl-arrow { - top: -0.55rem; - - /* Offset top padding */ - left: -1rem; - - /* Offset left padding */ -} - -.disp { - width: 5.5rem; -} - -.status { - max-width: 13rem; -} - -.actions { - width: 9rem; - text-align: right; - padding-top: 0; - padding-bottom: 0; -} - -.actions .btn { - margin-top: -0.35rem; - width: 6rem; - border-width: 2px; - font-weight: 700; -} - -.app-details { - border-top: 4px solid #fff; -} - -.app-comment-details td { - padding-top: 0; - padding-bottom: 1.25rem; -} - -.app-comment-details td a { - color: #007bff; -} - -.app-comment-details span+.loading-comment-info { - display: none; -} - -.cl-arrow::after { - content: ''; - position: absolute; - top: 0; - right: calc(var(--cl-arrow-size) * -1); - display: block; - width: var(--cl-arrow-size); - height: var(--cl-arrow-size); - border-style: solid; - border-top-width: calc(var(--cl-arrow-size) / 2); - border-top-color: transparent; - border-bottom-width: calc(var(--cl-arrow-size) / 2); - border-bottom-color: transparent; - border-left-width: calc(var(--cl-arrow-size) / 2); - border-left-color: var(--cl-arrow-color); -} - -.dropdown-menu { - padding-top: 0; - padding-bottom: 0; - border: none; - box-shadow: 0 0 0.5rem #999; - font-size: 0.875rem; -} - -.dropdown-menu button { - padding: 0.5rem 1rem; - color: #007bff; -} - -.dropdown-menu button i { - margin-top: -0.15rem; - vertical-align: middle; - margin-right: 0.25rem; - margin-left: -0.25rem; - font-size: 1.25rem; - width: 1.5rem; -} - -.dropdown-menu button:hover { - color: #fff; - background: #007bff; -} - -.dropdown-item:first-child { - border-top-left-radius: 0.25rem; - border-top-right-radius: 0.25rem; -} - -.dropdown-item:last-child { - border-bottom-left-radius: 0.25rem; - border-bottom-right-radius: 0.25rem; -} \ No newline at end of file diff --git a/src/app/metrics/metrics.component.html b/src/app/metrics/metrics.component.html deleted file mode 100644 index 58bdce8f7..000000000 --- a/src/app/metrics/metrics.component.html +++ /dev/null @@ -1,29 +0,0 @@ - -
-
- - - - - - - - - - - @for (item of data; track item) { - - - - - - - } - -
ActionUsermetaTimestamp
{{item.action || 'New Item'}}{{item.performedBy || item.addedBy }}{{item._objectSchema}}{{item.timestamp | date:'full'}}
-
-
diff --git a/src/app/metrics/metrics.component.ts b/src/app/metrics/metrics.component.ts deleted file mode 100644 index e27ab962e..000000000 --- a/src/app/metrics/metrics.component.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Component, ChangeDetectionStrategy } from '@angular/core'; -import { DatePipe } from '@angular/common'; - -@Component({ - changeDetection: ChangeDetectionStrategy.OnPush, - selector: 'app-metrics', - imports: [DatePipe], - templateUrl: './metrics.component.html', - styleUrl: './metrics.component.css', -}) -export class MetricsComponent { - public data: any[] = null; -} diff --git a/src/app/resolvers/full-project.resolver.spec.ts b/src/app/resolvers/full-project.resolver.spec.ts new file mode 100644 index 000000000..99a75e3d2 --- /dev/null +++ b/src/app/resolvers/full-project.resolver.spec.ts @@ -0,0 +1,75 @@ +import { TestBed } from '@angular/core/testing'; +import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; +import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; +import { convertToParamMap, type ActivatedRouteSnapshot, type RouterStateSnapshot } from '@angular/router'; +import { Observable, firstValueFrom, of } from 'rxjs'; + +import { fullProjectResolver } from './full-project.resolver'; +import { ApiService } from '../services/api'; +import { ConfigService } from '../services/config.service'; +import { KeycloakService } from '../services/keycloak.service'; +import { LoggingService } from '../services/logging.service'; +import { ProjectService } from '../services/project.service'; +import { ToastService } from '../services/toast.service'; + +const EAGLE_ID = '5885109faaecd9001b81314c'; + +describe('fullProjectResolver', () => { + let http: HttpTestingController; + let toast: jasmine.SpyObj; + + beforeEach(() => { + toast = jasmine.createSpyObj('ToastService', ['error']); + TestBed.configureTestingModule({ + providers: [ + provideHttpClient(withInterceptorsFromDi()), + provideHttpClientTesting(), + { provide: ConfigService, useValue: { config: () => ({}), getApiPath: () => '/api' } }, + { provide: KeycloakService, useValue: { keycloakEnabled: true } }, + { provide: ProjectService, useValue: { getPeopleObjs: (data: unknown) => of(data) } }, + { provide: LoggingService, useValue: jasmine.createSpyObj('LoggingService', ['error']) }, + { provide: ToastService, useValue: toast }, + ApiService + ] + }); + http = TestBed.inject(HttpTestingController); + }); + + afterEach(() => http.verify()); + + function resolve(projId = EAGLE_ID): Promise { + const route = { pathFromRoot: [{ paramMap: convertToParamMap({ projId }) }] } as unknown as ActivatedRouteSnapshot; + return firstValueFrom(TestBed.runInInjectionContext( + () => fullProjectResolver(route, {} as RouterStateSnapshot) as Observable + )); + } + + it('reads every legislation version of the project from eagle-api search', async () => { + const result = resolve(); + + const req = http.expectOne(r => r.url.startsWith('/api/search?')); + expect(req.request.url).toBe( + `/api/search?dataset=Project&pageNum=0&pageSize=1&projectLegislation=all&populate=true&and%5B_id%5D=${EAGLE_ID}` + ); + req.flush([{ searchResults: [{ legislationYearList: [2018] }], meta: [] }]); + + expect((await result)[0].data.searchResults[0].legislationYearList).toEqual([2018]); + }); + + it('skips eagle-api for a Track-only project reached by DEMI id', async () => { + const result = await resolve('2e9c6a4e-1b8d-4c51-9a3f-0d6c1f2b7e11'); + + http.expectNone(r => r.url.startsWith('/api/search')); + expect(result[0].data.searchResults).toEqual([]); + expect(toast.error).not.toHaveBeenCalled(); + }); + + it('yields no results and shows an error when the read fails', async () => { + const result = resolve(); + + http.expectOne(r => r.url.startsWith('/api/search?')).flush('boom', { status: 500, statusText: 'Server Error' }); + + expect((await result)[0].data.searchResults).toEqual([]); + expect(toast.error).toHaveBeenCalled(); + }); +}); diff --git a/src/app/resolvers/full-project.resolver.ts b/src/app/resolvers/full-project.resolver.ts index 570e4e805..72b890a17 100644 --- a/src/app/resolvers/full-project.resolver.ts +++ b/src/app/resolvers/full-project.resolver.ts @@ -1,15 +1,40 @@ import { inject } from '@angular/core'; -import { mergeMap } from 'rxjs/operators'; +import { of } from 'rxjs'; +import { catchError, map, mergeMap } from 'rxjs/operators'; import type { ActivatedRouteSnapshot, ResolveFn } from '@angular/router'; +import { isEagleId } from '../models/demi/demi-ids'; +import { SearchResults } from '../models/search'; +import { ApiService } from '../services/api'; +import { LoggingService } from '../services/logging.service'; import { ProjectService } from '../services/project.service'; -import { SearchService } from '../services/search.service'; +import { ToastService } from '../services/toast.service'; +const noResults = () => [new SearchResults({ data: { searchResults: [], meta: [{ searchResultsTotal: 0 }] } })]; + +/** + * Every legislation version of the project (`legislation_`), from eagle-api search, for the detail, + * archived-detail and edit screens. Stays on eagle-api until DEMI rows carry every legislation version. + */ export const fullProjectResolver: ResolveFn = (route: ActivatedRouteSnapshot) => { - const searchService = inject(SearchService); + const api = inject(ApiService); const projectService = inject(ProjectService); + const logger = inject(LoggingService); + const toast = inject(ToastService); const projId = route.pathFromRoot.find(r => r.paramMap.has('projId'))?.paramMap.get('projId') ?? ''; + // Track-only projects arrive by DEMI id and have no eagle-api record to read. + if (!isEagleId(projId)) { + return of(noResults()); + } + const qs = `search?dataset=Project&pageNum=0&pageSize=1&projectLegislation=all&populate=true&and%5B_id%5D=${projId}`; - return searchService.getSearchResults('', 'Project', [], 1, 1, '', { _id: projId }, true, {}, 'all') - .pipe(mergeMap(data => projectService.getPeopleObjs(data))); + return api.get(qs).pipe( + map(rows => rows.map(row => new SearchResults({ data: row }))), + catchError(error => { + logger.error('Failed to load project', 'fullProjectResolver', error); + toast.error('Failed to load project.'); + return of(noResults()); + }), + mergeMap(data => projectService.getPeopleObjs(data)) + ); }; diff --git a/src/app/search/search-document-table-rows/search-document-table-rows.component.html b/src/app/search/search-document-table-rows/search-document-table-rows.component.html index 3773cafb7..32dd31c49 100644 --- a/src/app/search/search-document-table-rows/search-document-table-rows.component.html +++ b/src/app/search/search-document-table-rows/search-document-table-rows.component.html @@ -11,8 +11,8 @@

{{document.datePosted | date:'longDate'}} - {{document.type | listConverter}} - {{document.milestone | listConverter}} + {{document.type || '-'}} + {{document.milestone || '-'}} {{document.legislation || '-'}} } \ No newline at end of file diff --git a/src/app/search/search-document-table-rows/search-document-table-rows.component.spec.ts b/src/app/search/search-document-table-rows/search-document-table-rows.component.spec.ts new file mode 100644 index 000000000..12ae587ff --- /dev/null +++ b/src/app/search/search-document-table-rows/search-document-table-rows.component.spec.ts @@ -0,0 +1,26 @@ +import { TestBed } from '@angular/core/testing'; +import { Router, provideRouter } from '@angular/router'; +import { SearchDocumentTableRowsComponent } from './search-document-table-rows.component'; + +describe('SearchDocumentTableRowsComponent', () => { + let component: SearchDocumentTableRowsComponent; + let navigate: jasmine.Spy; + + beforeEach(() => { + TestBed.configureTestingModule({ imports: [SearchDocumentTableRowsComponent], providers: [provideRouter([])] }); + navigate = spyOn(TestBed.inject(Router), 'navigate'); + component = TestBed.createComponent(SearchDocumentTableRowsComponent).componentInstance; + }); + + it('opens the document detail under its project', () => { + component.goToItem({ id: 'd1', projectId: 'p1' }); + + expect(navigate).toHaveBeenCalledOnceWith(['p', 'p1', 'project-documents', 'detail', 'd1']); + }); + + it('stays put when the row has no project', () => { + component.goToItem({ id: 'd1', projectId: undefined }); + + expect(navigate).not.toHaveBeenCalled(); + }); +}); diff --git a/src/app/search/search-document-table-rows/search-document-table-rows.component.ts b/src/app/search/search-document-table-rows/search-document-table-rows.component.ts index fdb56f582..9c449fa91 100644 --- a/src/app/search/search-document-table-rows/search-document-table-rows.component.ts +++ b/src/app/search/search-document-table-rows/search-document-table-rows.component.ts @@ -1,6 +1,5 @@ import { Component, OnInit, inject, ChangeDetectionStrategy, input } from '@angular/core'; import { DatePipe } from '@angular/common'; -import { ListConverterPipe } from 'src/app/shared/pipes/list-converter.pipe'; import { Router } from '@angular/router'; import { TableObject, TableColumn } from 'src/app/shared/components/table-template/table-object'; @@ -11,7 +10,7 @@ import { TableComponent } from 'src/app/shared/components/table-template/table.c selector: 'tbody[app-document-table-rows]', templateUrl: './search-document-table-rows.component.html', styleUrl: './search-document-table-rows.component.css', - imports: [DatePipe, ListConverterPipe], + imports: [DatePipe], }) export class SearchDocumentTableRowsComponent implements OnInit, TableComponent { @@ -23,7 +22,6 @@ export class SearchDocumentTableRowsComponent implements OnInit, TableComponent public documents: any; public paginationData: any; - public activeLegislationYear: number; public columns: TableColumn[]; public useSmallTable: boolean; @@ -35,12 +33,10 @@ export class SearchDocumentTableRowsComponent implements OnInit, TableComponent } goToItem(item) { - this.activeLegislationYear = 0; - // This happens when the api has not done the lookup on the project id - // And we get just the string back and not the project object - if (item.project && typeof item.project === 'object' && '_id' in item.project) { - this.router.navigate(['p', item.project._id, 'project-documents', 'detail', item._id]); + if (!item.projectId) { + return; } + this.router.navigate(['p', item.projectId, 'project-documents', 'detail', item.id]); } } diff --git a/src/app/search/search.component.css b/src/app/search/search.component.css index 928d6e340..17df4a82b 100644 --- a/src/app/search/search.component.css +++ b/src/app/search/search.component.css @@ -124,14 +124,6 @@ form label { margin-left: 172px; } -.advanced-search .pcp-filters { - margin-left: 335px; -} - -.advanced-search .pcp-filters .filter-border { - min-width: 230px; -} - .advanced-search .more-filters { margin-left: 8px; } @@ -185,10 +177,6 @@ form label { width: 155px; } -.advanced-search .pcp-btn { - width: 230px; -} - .advanced-search .milestone-btn { width: 130px; } diff --git a/src/app/search/search.component.html b/src/app/search/search.component.html index e125b3ad8..93c11b2e0 100644 --- a/src/app/search/search.component.html +++ b/src/app/search/search.component.html @@ -54,15 +54,6 @@

Search Environmental Assessment Projects

numFilters.eacDecision }}) } -