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/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