Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
c9ce28d
extract BaseWorkspacePersistenceService
arlowhite Nov 19, 2025
2bb62dc
display errors, minor repairs ok (consider individual tabs)
arlowhite Nov 19, 2025
55f48df
move version to property
arlowhite Nov 19, 2025
f59533a
fix error on new project, improve design
arlowhite Nov 19, 2025
571d6f0
fix save project on delete/close tab
arlowhite Nov 19, 2025
1911c67
improve projectId management and service providing
arlowhite Nov 21, 2025
985cd32
Show dialog when project fails to load, no longer fallback to local s…
arlowhite Nov 25, 2025
0647736
Add AI guides from ng new project
arlowhite Nov 25, 2025
1fb57d5
Move delete workspace tab to menu
arlowhite Nov 25, 2025
befac4c
cleanup ADRIA project CSS
arlowhite Nov 25, 2025
4c4bfd7
cleanup :deep style halucinations that have no effect
arlowhite Nov 25, 2025
a986aa7
improve new tab button css
arlowhite Nov 25, 2025
a7fe868
reduce .workflow-header padding, format
arlowhite Nov 25, 2025
cb6f197
Site Assessment save working, some persistence service refactoring an…
arlowhite Nov 26, 2025
b88d701
fix Assess button enabled
arlowhite Nov 26, 2025
0703881
initialState$ work, cannot save before loading initial, patch state m…
arlowhite Nov 27, 2025
eae0038
job layers on load initial state
arlowhite Nov 27, 2025
5c752e6
cleanup form, fix state bug, clear removes job layers
arlowhite Nov 27, 2025
e2579e2
debounce backend save
arlowhite Nov 27, 2025
79f653b
remove null, some cleanup
arlowhite Nov 27, 2025
323d276
remove repair param
arlowhite Nov 27, 2025
a2654e4
rxjs cleanup
arlowhite Nov 27, 2025
4f1a656
project loading overlay
arlowhite Nov 27, 2025
2bf8f94
persist Map View
arlowhite Nov 30, 2025
3cf1d5a
format
arlowhite Nov 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions packages/app/.claude/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
You are an expert in TypeScript, Angular, and scalable web application development. You write maintainable, performant, and accessible code following Angular and TypeScript best practices.

## TypeScript Best Practices

- Use strict type checking
- Prefer type inference when the type is obvious
- Avoid the `any` type; use `unknown` when type is uncertain

## Angular Best Practices

- Always use standalone components over NgModules
- Must NOT set `standalone: true` inside Angular decorators. It's the default.
- Use signals for state management
- Implement lazy loading for feature routes
- Do NOT use the `@HostBinding` and `@HostListener` decorators. Put host bindings inside the `host` object of the `@Component` or `@Directive` decorator instead
- Use `NgOptimizedImage` for all static images.
- `NgOptimizedImage` does not work for inline base64 images.

## Components

- Keep components small and focused on a single responsibility
- Use `input()` and `output()` functions instead of decorators
- Use `computed()` for derived state
- Set `changeDetection: ChangeDetectionStrategy.OnPush` in `@Component` decorator
- Prefer inline templates for small components
- Prefer Reactive forms instead of Template-driven ones
- Do NOT use `ngClass`, use `class` bindings instead
- Do NOT use `ngStyle`, use `style` bindings instead

## State Management

- Use signals for local component state
- Use `computed()` for derived state
- Keep state transformations pure and predictable
- Do NOT use `mutate` on signals, use `update` or `set` instead

## Templates

- Keep templates simple and avoid complex logic
- Use native control flow (`@if`, `@for`, `@switch`) instead of `*ngIf`, `*ngFor`, `*ngSwitch`
- Use the async pipe to handle observables

## Services

- Design services around a single responsibility
- Use the `providedIn: 'root'` option for singleton services
- Use the `inject()` function instead of constructor injection
47 changes: 47 additions & 0 deletions packages/app/.github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
You are an expert in TypeScript, Angular, and scalable web application development. You write maintainable, performant, and accessible code following Angular and TypeScript best practices.

## TypeScript Best Practices

- Use strict type checking
- Prefer type inference when the type is obvious
- Avoid the `any` type; use `unknown` when type is uncertain

## Angular Best Practices

- Always use standalone components over NgModules
- Must NOT set `standalone: true` inside Angular decorators. It's the default.
- Use signals for state management
- Implement lazy loading for feature routes
- Do NOT use the `@HostBinding` and `@HostListener` decorators. Put host bindings inside the `host` object of the `@Component` or `@Directive` decorator instead
- Use `NgOptimizedImage` for all static images.
- `NgOptimizedImage` does not work for inline base64 images.

## Components

- Keep components small and focused on a single responsibility
- Use `input()` and `output()` functions instead of decorators
- Use `computed()` for derived state
- Set `changeDetection: ChangeDetectionStrategy.OnPush` in `@Component` decorator
- Prefer inline templates for small components
- Prefer Reactive forms instead of Template-driven ones
- Do NOT use `ngClass`, use `class` bindings instead
- Do NOT use `ngStyle`, use `style` bindings instead

## State Management

- Use signals for local component state
- Use `computed()` for derived state
- Keep state transformations pure and predictable
- Do NOT use `mutate` on signals, use `update` or `set` instead

## Templates

- Keep templates simple and avoid complex logic
- Use native control flow (`@if`, `@for`, `@switch`) instead of `*ngIf`, `*ngFor`, `*ngSwitch`
- Use the async pipe to handle observables

## Services

- Design services around a single responsibility
- Use the `providedIn: 'root'` option for singleton services
- Use the `inject()` function instead of constructor injection
19 changes: 19 additions & 0 deletions packages/app/docs/dev_tips.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,24 @@ which is a good guideline for modern Angular.

### Debugging

[Angular DevTools Extenion](https://angular.dev/tools/devtools) for Chrome and Firefox.

Chrome Dev Tools now has support for
[Angular performance profiling](https://angular.dev/best-practices/profiling-with-chrome-devtools).

## RxJS

### Debugging

The `rxjs-util.ts` file has a `tapDebug` function that can be helpful.

### Error Management
A function that returns an `Observable` should throw errors via an `Observable`; otherwise there are two error paths: synchronous, and via the observable, which is confusing.

```javascript
return throwError(() => new Error('my error'));
```

### Subscription Cleanup

Cleanup un-tracked subscriptions tied to the life of a component using [takeUntilDestroyed](https://angular.dev/ecosystem/rxjs-interop/take-until-destroyed)
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { CommonModule } from '@angular/common';
import { Component, inject, DestroyRef, signal } from '@angular/core';
import { Component, DestroyRef, inject, signal } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
Expand All @@ -9,8 +9,7 @@ import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSnackBar } from '@angular/material/snack-bar';
import { catchError, finalize, tap } from 'rxjs/operators';
import { of } from 'rxjs';
import { catchError, finalize, of, tap } from 'rxjs';
import { MatIconModule } from '@angular/material/icon';
import { WebApiService } from '../../../../api/web-api.service';
import { UserRole } from '@reefguide/db';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSelectModule } from '@angular/material/select';
import { MatSnackBar } from '@angular/material/snack-bar';
import { User } from '@reefguide/db';
import { of } from 'rxjs';
import { catchError, finalize, tap } from 'rxjs/operators';
import { catchError, finalize, of, tap } from 'rxjs';
import { WebApiService } from '../../../../api/web-api.service';

interface UpdatePasswordForm {
Expand Down
5 changes: 3 additions & 2 deletions packages/app/src/app/app.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { provideHttpClient, withFetch, withInterceptors } from '@angular/common/http';
import { ApplicationConfig, ErrorHandler, provideZonelessChangeDetection } from '@angular/core';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { provideRouter, withComponentInputBinding } from '@angular/router';
import { provideRouter, withComponentInputBinding, withViewTransitions } from '@angular/router';
import { routes } from './app.routes';
import { authInterceptor } from './auth/auth-http-interceptor';
import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';
Expand All @@ -26,7 +26,8 @@ export const appConfig: ApplicationConfig = {
provideZonelessChangeDetection(),
// For debugging change detection, exhaustive false by default
// provideCheckNoChangesConfig({ exhaustive: true, interval: 1_000}),
provideRouter(routes, withComponentInputBinding()),
provideRouter(routes, withComponentInputBinding(), withViewTransitions()),
// TODO remove uses of old animations system
provideAnimationsAsync(),
provideHttpClient(withFetch(), withInterceptors([authInterceptor])),

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
@if (isBusy) {
<mat-progress-spinner mode="indeterminate" diameter="32"> </mat-progress-spinner>
}
@let formStatus = criteria.form.statusChanges | async;
@let formStatus = criteria.formStatus$ | async;
<button
mat-flat-button
[disabled]="isBusy || formStatus !== 'VALID'"
Expand Down Expand Up @@ -103,3 +103,7 @@
<app-profile-button />
</mat-drawer-content>
</mat-drawer-container>
<!-- TODO fade-out animation in Angular 21 -->
@if (persistenceService.isInitialStateLoading()) {
<app-loading-overlay></app-loading-overlay>
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import { AsyncPipe, CommonModule } from '@angular/common';
import { Component, effect, inject, signal, viewChild, ViewChild } from '@angular/core';
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
import {
Component,
effect,
inject,
input,
numberAttribute,
signal,
viewChild,
ViewChild
} from '@angular/core';
import { toObservable } from '@angular/core/rxjs-interop';
import { MatButtonModule } from '@angular/material/button';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatIconModule } from '@angular/material/icon';
Expand All @@ -11,7 +20,7 @@ import { MatSnackBar } from '@angular/material/snack-bar';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatTooltip } from '@angular/material/tooltip';
import { fromLonLat } from 'ol/proj';
import { combineLatest, map, Observable } from 'rxjs';
import { combineLatest, map, Observable, take } from 'rxjs';
import { WebApiService } from '../../api/web-api.service';
import { AuthService } from '../auth/auth.service';
import { ReefMapComponent } from '../reef-map/reef-map.component';
Expand All @@ -23,8 +32,12 @@ import { MAP_UI, MapUI, ReefGuideMapService } from './reef-guide-map.service';
import { SelectionCriteriaComponent } from './selection-criteria/selection-criteria.component';
import { MapToolbarComponent } from './map-toolbar/map-toolbar.component';
import { PolygonMapService } from './polygon-map.service';
import { ActivatedRoute } from '@angular/router';
import BaseLayer from 'ol/layer/Base';
import {
WorkspacePersistenceService,
WorkspaceState
} from './persistence/workspace-persistence.service';
import { LoadingOverlayComponent } from '../widgets/loading-overlay/loading-overlay.component';

type DrawerModes = 'criteria' | 'style';

Expand All @@ -51,12 +64,14 @@ type DrawerModes = 'criteria' | 'style';
ReefMapComponent,
LayerStyleEditorComponent,
ProfileButtonComponent,
MapToolbarComponent
MapToolbarComponent,
LoadingOverlayComponent
],
providers: [
ReefGuideMapService,
PolygonMapService,
{ provide: MAP_UI, useExisting: LocationSelectionComponent }
{ provide: MAP_UI, useExisting: LocationSelectionComponent },
WorkspacePersistenceService
],
templateUrl: './location-selection.component.html',
styleUrl: './location-selection.component.scss'
Expand All @@ -66,9 +81,14 @@ export class LocationSelectionComponent implements MapUI {
readonly authService = inject(AuthService);
readonly api = inject(WebApiService);
readonly mapService = inject(ReefGuideMapService);
readonly persistenceService = inject(WorkspacePersistenceService);
private readonly snackbar = inject(MatSnackBar);
private activatedRoute = inject(ActivatedRoute);
private projectId = toSignal<string>(this.activatedRoute.params.pipe(map(p => p['projectId'])));

/**
* Current project ID
* via route param
*/
public readonly projectId = input(undefined, { transform: numberAttribute });

map = viewChild.required(ReefMapComponent);

Expand Down Expand Up @@ -106,6 +126,27 @@ export class LocationSelectionComponent implements MapUI {
},
{ manualCleanup: true }
);

// warm-up so request starts before panel is opened
this.persistenceService.initialState$.pipe(take(1)).subscribe(state => {
this.onLoadInitialState(state);
});
}

onLoadInitialState(state: WorkspaceState) {
const { regionalAssessmentJob, suitabilityAssessmentJob } = state;
if (regionalAssessmentJob) {
console.log('loading saved regional assessment job', regionalAssessmentJob);
this.mapService
.loadLayerFromJobResults(regionalAssessmentJob.jobId, regionalAssessmentJob.region)
.subscribe();
}
if (suitabilityAssessmentJob) {
console.log('loading saved suitability assessment job', suitabilityAssessmentJob);
this.mapService
.loadLayerFromJobResults(suitabilityAssessmentJob.jobId, suitabilityAssessmentJob.region)
.subscribe();
}
}

openLayerStyleEditor(layer: BaseLayer): void {
Expand Down Expand Up @@ -162,8 +203,6 @@ export class LocationSelectionComponent implements MapUI {
void this.drawer.close();

this.mapService.addRegionalAssessmentJob(regionalAssessment);
// could load previous job result like this:
// this.mapService.loadLayerFromJobResults(31);

if (suitabilityAssessment) {
this.mapService.addSuitabilityAssessmentJob(suitabilityAssessment);
Expand All @@ -176,10 +215,11 @@ export class LocationSelectionComponent implements MapUI {
*/
onPolygonDrawn(geojson: string): void {
try {
const projectId = this.projectId();
const polygon = JSON.parse(geojson);

// Create the polygon via API
if (!this.projectId()) {
if (projectId == null) {
// Routing stuffed up here!
console.error('There is no project ID, this route should not have loaded!');
this.snackbar.open(
Expand All @@ -190,14 +230,14 @@ export class LocationSelectionComponent implements MapUI {
}
);
} else {
this.api.createPolygon({ polygon, projectId: parseInt(this.projectId()!) }).subscribe({
this.api.createPolygon({ polygon, projectId }).subscribe({
next: () => {
this.snackbar.open('Polygon saved successfully', 'OK', {
duration: 3000
});

// Refresh the polygon layer on the map
this.mapService.polygonMapService.refresh(parseInt(this.projectId()!));
this.mapService.polygonMapService.refresh(projectId);
},
error: error => {
console.error('Error creating polygon:', error);
Expand Down
Loading
Loading