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 @@
-
-
-
-
-
-
- | Action |
- User |
- meta |
- Timestamp |
-
-
-
- @for (item of data; track item) {
-
- | {{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
{{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 }})
}
-