@nauticana/sail 1.0.8 → 1.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nauticana/sail",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
4
4
  "description": "Metadata-driven Angular component library for CRUD admin frontends backed by the keel Go backend. Tables, forms, navigation, OTP/2FA/social auth, billing, payout onboarding.",
5
5
  "main": "src/index.ts",
6
6
  "scripts": {
@@ -38,7 +38,7 @@
38
38
  </td>
39
39
  }
40
40
  <td>
41
- <button matIconButton (click)="saveRow()" title="Save"><mat-icon>save</mat-icon></button>
41
+ <button matIconButton (click)="saveRow()" [title]="isNew ? 'Add' : 'Update'"><mat-icon>{{ isNew ? 'add' : 'check' }}</mat-icon></button>
42
42
  <button matIconButton (click)="cancelRow(record)" title="Cancel"><mat-icon>cancel</mat-icon></button>
43
43
  </td>
44
44
  } @else {
@@ -1,4 +1,4 @@
1
- import { ChangeDetectionStrategy, Component, effect, inject, input, linkedSignal, OnInit, ViewEncapsulation } from "@angular/core";
1
+ import { ChangeDetectionStrategy, Component, effect, inject, input, linkedSignal, OnInit, output, ViewEncapsulation } from "@angular/core";
2
2
  import { DynamicField } from "../form/form_field";
3
3
  import { MatButtonModule } from "@angular/material/button";
4
4
  import { BaseForm } from "../abstract/base_form";
@@ -26,6 +26,9 @@ export class TableDetail extends BaseForm implements OnInit {
26
26
  readonly parentRecord = input<any>({});
27
27
  /** When true, the parent view is in read-only mode — disable all child row editing. */
28
28
  readonly parentReadOnly = input(false);
29
+ /** Fires after any grid mutation (add/edit/delete/undelete/cancel) so a parent
30
+ * can re-evaluate unsaved-changes state. Local only — no server call. */
31
+ readonly changed = output<void>();
29
32
 
30
33
  override readonly tableName = linkedSignal<string, string>({ source: () => this.tableNameInput(), computation: (v, p) => v || p?.value || '' });
31
34
 
@@ -88,6 +91,7 @@ export class TableDetail extends BaseForm implements OnInit {
88
91
  } else {
89
92
  Object.assign(record, result);
90
93
  }
94
+ this.changed.emit();
91
95
  }
92
96
  });
93
97
  }
@@ -123,6 +127,7 @@ export class TableDetail extends BaseForm implements OnInit {
123
127
  this.editingRecord = null;
124
128
  this.originalRecord = null;
125
129
  this.isNew = false;
130
+ this.changed.emit();
126
131
  }
127
132
 
128
133
  cancelRow(record: any) {
@@ -137,6 +142,7 @@ export class TableDetail extends BaseForm implements OnInit {
137
142
  this.editingRecord = null;
138
143
  this.originalRecord = null;
139
144
  this.isNew = false;
145
+ this.changed.emit();
140
146
  }
141
147
 
142
148
  deleteRow(record: any) {
@@ -148,11 +154,13 @@ export class TableDetail extends BaseForm implements OnInit {
148
154
  } else {
149
155
  record[this.config.opField] = OpCode.Delete;
150
156
  }
157
+ this.changed.emit();
151
158
  }
152
159
 
153
160
  unDeleteRow(record: any) {
154
161
  if (record[this.config.opField] === OpCode.Delete) {
155
162
  record[this.config.opField] = OpCode.Update;
156
163
  }
164
+ this.changed.emit();
157
165
  }
158
166
  }
@@ -12,7 +12,7 @@ import {
12
12
  import { RestURL } from './rest_url';
13
13
  import { Observable, ReplaySubject } from 'rxjs';
14
14
  import { ApplicationMenu, ConstantValue } from '../model/common';
15
- import { CanActivateFn, Router, Routes } from '@angular/router';
15
+ import { CanActivateFn, CanDeactivateFn, Router, Routes } from '@angular/router';
16
16
  import { SAIL_GUI_CONFIG, SailGuiConfig, DEFAULT_CONFIG } from '../config';
17
17
  import { BaseRestService } from './base_rest.service';
18
18
 
@@ -500,6 +500,15 @@ export abstract class BaseAuthService extends BaseRestService {
500
500
  );
501
501
  };
502
502
 
503
+ // Confirms navigation away when the routed component reports unsaved changes.
504
+ // Opt in by implementing hasUnsavedChanges(): boolean on the component.
505
+ readonly canDeactivate: CanDeactivateFn<{ hasUnsavedChanges?: () => boolean }> = (component) => {
506
+ if (component && typeof component.hasUnsavedChanges === 'function' && component.hasUnsavedChanges()) {
507
+ return window.confirm('You have unsaved changes. Leave this page without saving?');
508
+ }
509
+ return true;
510
+ };
511
+
503
512
  /** Override in subclasses to add project-specific routes */
504
513
  protected buildExtraRoutes(data: ApplicationData): Routes {
505
514
  if (this.guiConfig.extraRoutes) {
@@ -615,6 +624,13 @@ export abstract class BaseAuthService extends BaseRestService {
615
624
  }
616
625
  }
617
626
 
627
+ // Guard navigation away from any component-backed page that
628
+ // tracks unsaved changes (opt-in via hasUnsavedChanges()).
629
+ for (const r of children) {
630
+ if ((r as { component?: unknown }).component) {
631
+ (r as { canDeactivate?: unknown[] }).canDeactivate = [this.canDeactivate];
632
+ }
633
+ }
618
634
  newRoutes.push({ path: menuPath, children });
619
635
  }
620
636