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/models/demi/project.ts b/src/app/models/demi/project.ts index debefc71b..580bc2d5b 100644 --- a/src/app/models/demi/project.ts +++ b/src/app/models/demi/project.ts @@ -43,3 +43,23 @@ export interface DemiProject { pins?: DemiPin[]; [field: string]: unknown; } + +/** A project list multi-select: the URL holds each picked item's `by`; DEMI filter `api` gets each item's `apiKey`. */ +export interface ProjectFilter { + by: string; + api: string; + apiKey: string; +} + +/** + * Project list filters by URL param name, for the project list and search pages. + * DEMI region List rows have no code, and DEMI filters proponents by name. + */ +export const PROJECT_FILTERS: Record<'region' | 'CEAAInvolvement' | 'proponent' | 'eacDecision' | 'type' | 'projectPhase', ProjectFilter> = { + region: { by: 'name', api: 'region', apiKey: 'name' }, + CEAAInvolvement: { by: 'id', api: 'CEAAInvolvement', apiKey: 'id' }, + proponent: { by: 'id', api: 'proponentName', apiKey: 'name' }, + eacDecision: { by: 'id', api: 'eacDecision', apiKey: 'id' }, + type: { by: 'name', api: 'projectType', apiKey: 'name' }, + projectPhase: { by: 'id', api: 'currentPhaseName', apiKey: 'id' } +}; diff --git a/src/app/project/project-documents/application-sort/application-sort-table-rows/application-sort-table-rows.component.spec.ts b/src/app/project/project-documents/application-sort/application-sort-table-rows/application-sort-table-rows.component.spec.ts new file mode 100644 index 000000000..693a00dd7 --- /dev/null +++ b/src/app/project/project-documents/application-sort/application-sort-table-rows/application-sort-table-rows.component.spec.ts @@ -0,0 +1,31 @@ +import { TestBed } from '@angular/core/testing'; +import { ApplicationSortTableRowsComponent } from './application-sort-table-rows.component'; +import { StorageService } from 'src/app/services/storage.service'; + +describe('ApplicationSortTableRowsComponent', () => { + let state: { editedDocs: any[] }; + let component: ApplicationSortTableRowsComponent; + + beforeEach(() => { + state = { editedDocs: [] }; + TestBed.configureTestingModule({ + imports: [ApplicationSortTableRowsComponent], + providers: [{ provide: StorageService, useValue: { state } }] + }); + component = TestBed.createComponent(ApplicationSortTableRowsComponent).componentInstance; + }); + + it('keeps every edited row that has no Eagle id', () => { + component.updateSelectedCount({ id: 'uuid-1', _id: null, sortOrder: 1 }); + component.updateSelectedCount({ id: 'uuid-2', _id: null, sortOrder: 2 }); + + expect(state.editedDocs.map(doc => doc.id)).toEqual(['uuid-1', 'uuid-2']); + }); + + it('keeps the latest edit of a row seen again on a later page load', () => { + component.updateSelectedCount({ id: 'doc-1', _id: 'doc-1', sortOrder: 5 }); + component.updateSelectedCount({ id: 'doc-1', _id: 'doc-1', sortOrder: 7 }); + + expect(state.editedDocs.map(doc => doc.sortOrder)).toEqual([7]); + }); +}); diff --git a/src/app/project/project-documents/application-sort/application-sort-table-rows/application-sort-table-rows.component.ts b/src/app/project/project-documents/application-sort/application-sort-table-rows/application-sort-table-rows.component.ts index aba973444..5f726ba40 100644 --- a/src/app/project/project-documents/application-sort/application-sort-table-rows/application-sort-table-rows.component.ts +++ b/src/app/project/project-documents/application-sort/application-sort-table-rows/application-sort-table-rows.component.ts @@ -38,20 +38,14 @@ export class ApplicationSortTableRowsComponent implements OnInit, TableComponent updateSelectedCount(editedDoc: any): void { this.selectedCount.emit(editedDoc); - if (Array.isArray(this.storageService.state.editedDocs)) { - // check if it exists already - let docWasEditedPreviously = false; - this.storageService.state.editedDocs.forEach((previouslyEditedDoc: any) => { - if (previouslyEditedDoc._id === editedDoc._id) { - // update entry - previouslyEditedDoc = editedDoc; - docWasEditedPreviously = true; - return; - } - }); - - if (!docWasEditedPreviously) { - this.storageService.state.editedDocs.push(editedDoc); + const edited = this.storageService.state.editedDocs; + if (Array.isArray(edited)) { + // By `id`: rows not in Eagle yet all have no `_id`. + const index = edited.findIndex((doc: any) => doc.id === editedDoc.id); + if (index === -1) { + edited.push(editedDoc); + } else { + edited[index] = editedDoc; } } } diff --git a/src/app/project/project-documents/application-sort/application-sort.component.spec.ts b/src/app/project/project-documents/application-sort/application-sort.component.spec.ts index af725696b..cc819ff9a 100644 --- a/src/app/project/project-documents/application-sort/application-sort.component.spec.ts +++ b/src/app/project/project-documents/application-sort/application-sort.component.spec.ts @@ -1,7 +1,7 @@ import { TestBed } from '@angular/core/testing'; import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; import { HttpTestingController, TestRequest, provideHttpClientTesting } from '@angular/common/http/testing'; -import { ActivatedRoute, provideRouter } from '@angular/router'; +import { ActivatedRoute, Router, provideRouter } from '@angular/router'; import { of } from 'rxjs'; import { DocumentApplicationSortComponent } from './application-sort.component'; @@ -9,6 +9,7 @@ import { ConfigService } from 'src/app/services/config.service'; import { KeycloakService } from 'src/app/services/keycloak.service'; import { LoggingService } from 'src/app/services/logging.service'; import { StorageService } from 'src/app/services/storage.service'; +import { ToastService } from 'src/app/services/toast.service'; import { TableTemplateUtils } from 'src/app/shared/utils/table-template-utils'; const LISTS = [ @@ -111,4 +112,35 @@ describe('DocumentApplicationSortComponent', () => { expect(component.documents[0].sortOrder).toBe(1); }); + + describe('save', () => { + let warning: jasmine.Spy; + + function save(editedDocs: any[]) { + const component = init({}); + documentsRequest().flush([]); + storage.state.editedDocs = editedDocs; + warning = spyOn(TestBed.inject(ToastService), 'warning'); + spyOn(TestBed.inject(Router), 'navigate'); + component.onSave(); + } + + it('names the documents it skips because they have no Eagle id', () => { + save([ + { _id: 'doc-eagle-1', sortOrder: 1, displayName: 'Application.pdf' }, + { _id: null, id: 'uuid-1', sortOrder: 2, displayName: 'New.pdf' }, + { _id: null, id: 'uuid-2', sortOrder: 3, displayName: '' } + ]); + + expect(http.expectOne('/api/document/doc-eagle-1').request.method).toBe('PUT'); + expect(warning).toHaveBeenCalledOnceWith('Sort order not saved for 2 document(s) not in Eagle yet: New.pdf, uuid-2'); + }); + + it('warns about nothing when every document has an Eagle id', () => { + save([{ _id: 'doc-eagle-1', sortOrder: 1, displayName: 'Application.pdf' }]); + + http.expectOne('/api/document/doc-eagle-1'); + expect(warning).not.toHaveBeenCalled(); + }); + }); }); diff --git a/src/app/project/project-documents/application-sort/application-sort.component.ts b/src/app/project/project-documents/application-sort/application-sort.component.ts index cd3150d80..9eb0e0bd3 100644 --- a/src/app/project/project-documents/application-sort/application-sort.component.ts +++ b/src/app/project/project-documents/application-sort/application-sort.component.ts @@ -157,8 +157,14 @@ export class DocumentApplicationSortComponent implements OnInit { onSave() { const formData = new FormData(); + const edited: any[] = this.storageService.state.editedDocs; // A document with no Eagle id cannot be written through eagle-api. - this.storageService.state.editedDocs.filter((document: any) => document._id).forEach((document: any) => { + const skipped = edited.filter(document => !document._id); + if (skipped.length) { + const names = skipped.map(document => document.displayName || document.id).join(', '); + this.toastService.warning(`Sort order not saved for ${skipped.length} document(s) not in Eagle yet: ${names}`); + } + edited.filter(document => document._id).forEach((document: any) => { // document service put id and sort order formData.set('sortOrder', document.sortOrder); this.documentService.update(formData, document._id) diff --git a/src/app/projects/project-list/project-list.component.ts b/src/app/projects/project-list/project-list.component.ts index f34d0d165..41ca32ad6 100644 --- a/src/app/projects/project-list/project-list.component.ts +++ b/src/app/projects/project-list/project-list.component.ts @@ -10,7 +10,7 @@ import { NgbDatepickerModule } from '@ng-bootstrap/ng-bootstrap'; import { DateTime } from 'luxon'; import { DEMI_MAX_PAGE_SIZE, DemiList } from 'src/app/models/demi/demi-list'; import { Proponent } from 'src/app/models/demi/organization'; -import { DemiProject } from 'src/app/models/demi/project'; +import { DemiProject, PROJECT_FILTERS, ProjectFilter } from 'src/app/models/demi/project'; import { LegacyProject, toLegacyProject } from 'src/app/models/demi/project-legacy'; import { SearchTerms } from 'src/app/models/search'; import { ConfigService } from 'src/app/services/config.service'; @@ -221,24 +221,36 @@ export class ProjectListComponent implements OnInit { this.router.navigate(['/projects', 'add', 'form-2018']); } - /** Reads `name` from the URL; sends each picked record's `apiKey` to DEMI as `apiName`. */ - paramsToCollectionFilters(params, name: string, collection: any[], identifyBy: string, apiName: string, apiKey: string) { + /** Reads `name` from the URL; sends each picked record's `apiKey` to DEMI as `api`. */ + paramsToCollectionFilters(params, name: string, collection: any[], { by, api, apiKey }: ProjectFilter) { this.filterForUI[name] = []; delete this.filterForURL[name]; - delete this.filterForAPI[apiName]; + delete this.filterForAPI[api]; if (params[name] && collection) { const records = params[name].split(',') - .map(value => collection.find(item => item[identifyBy] === value)) + .map(value => collection.find(item => item[by] === value)) .filter(Boolean); if (records.length) { this.filterForUI[name] = records; - this.filterForURL[name] = records.map(r => r[identifyBy]).join(','); - this.filterForAPI[apiName] = records.map(r => r[apiKey]); + this.filterForURL[name] = records.map(r => r[by]).join(','); + this.filterForAPI[api] = records.map(r => r[apiKey]); } } } + /** Picker options for each of {@link PROJECT_FILTERS}. */ + private filterItems(): Record { + return { + region: this.regions, + CEAAInvolvement: this.ceaaInvolvements, + proponent: this.proponents, + eacDecision: this.eacDecisions, + type: this.projectTypes, + projectPhase: this.projectPhases + }; + } + paramsToDateFilters(params, name) { this.filterForUI[name] = null; delete this.filterForURL[name]; @@ -258,13 +270,10 @@ export class ProjectListComponent implements OnInit { } setFiltersFromParams(params) { - // DEMI region List rows have no code, and DEMI filters proponents by name. - this.paramsToCollectionFilters(params, 'region', this.regions, 'name', 'region', 'name'); - this.paramsToCollectionFilters(params, 'CEAAInvolvement', this.ceaaInvolvements, 'id', 'CEAAInvolvement', 'id'); - this.paramsToCollectionFilters(params, 'proponent', this.proponents, 'id', 'proponentName', 'name'); - this.paramsToCollectionFilters(params, 'eacDecision', this.eacDecisions, 'id', 'eacDecision', 'id'); - this.paramsToCollectionFilters(params, 'type', this.projectTypes, 'name', 'projectType', 'name'); - this.paramsToCollectionFilters(params, 'projectPhase', this.projectPhases, 'id', 'currentPhaseName', 'id'); + const items = this.filterItems(); + for (const [name, filter] of Object.entries(PROJECT_FILTERS)) { + this.paramsToCollectionFilters(params, name, items[name], filter); + } this.paramsToDateFilters(params, 'decisionDateStart'); this.paramsToDateFilters(params, 'decisionDateEnd'); @@ -295,12 +304,9 @@ export class ProjectListComponent implements OnInit { } setParamsFromFilters(params) { - this.collectionFilterToParams(params, 'region', 'name'); - this.collectionFilterToParams(params, 'CEAAInvolvement', 'id'); - this.collectionFilterToParams(params, 'proponent', 'id'); - this.collectionFilterToParams(params, 'eacDecision', 'id'); - this.collectionFilterToParams(params, 'type', 'name'); - this.collectionFilterToParams(params, 'projectPhase', 'id'); + for (const [name, filter] of Object.entries(PROJECT_FILTERS)) { + this.collectionFilterToParams(params, name, filter.by); + } this.dateFilterToParams(params, 'decisionDateStart'); this.dateFilterToParams(params, 'decisionDateEnd'); 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 }}) } -