@nauticana/sail 1.1.0 → 1.1.1

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.1.0",
3
+ "version": "1.1.1",
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": {
@@ -1,10 +1,12 @@
1
- import { inject, signal, WritableSignal } from "@angular/core";
1
+ import { inject, signal, WritableSignal, Injector } from "@angular/core";
2
2
  import { ForeignKey, TableAction, TableColumn, TableDefinition } from "../../model/appdata";
3
3
  import { ConstantValue, LookupStyle, OpCode } from "../../model/common";
4
4
  import { BaseAuthService } from "../../service/auth.service";
5
5
  import { BackendService } from "../../service/rest_service";
6
6
  import { SAIL_GUI_CONFIG, SailGuiConfig, DEFAULT_CONFIG } from "../../config";
7
7
  import { titleCase } from "../../util/text";
8
+ import { MatDialog } from "@angular/material/dialog";
9
+ import { RevealDialog } from "../table/reveal_dialog";
8
10
 
9
11
 
10
12
  /** CRUD verb used by `requireAuth()` to compose the "Missing authorization" alert. */
@@ -14,6 +16,8 @@ export type AuthVerb = 'create' | 'read' | 'update' | 'delete' | 'view';
14
16
  export abstract class BaseTable {
15
17
  protected readonly cacheService = inject(BaseAuthService);
16
18
  protected readonly backendService = inject(BackendService);
19
+ // Reveal dialogs are rare — resolve MatDialog lazily so every BaseTable subclass doesn't eagerly depend on it.
20
+ readonly #injector = inject(Injector);
17
21
  protected readonly config: SailGuiConfig = inject(SAIL_GUI_CONFIG, {optional: true}) ?? DEFAULT_CONFIG;
18
22
  protected readonly dialogWidth: string = '400px';
19
23
  readonly tableName: WritableSignal<string> = signal('');
@@ -114,15 +118,18 @@ export abstract class BaseTable {
114
118
  if (!this.beforeExecuteAction(action, record)) return;
115
119
  if (action.confirmMessage && !confirm(action.confirmMessage)) return;
116
120
  const body = action.recordSpecific && record ? this.primaryKeyValues(record) : {};
117
- this.backendService.executeAction<{ url?: string }>(action.method, body).subscribe({
121
+ this.backendService.executeAction<Record<string, unknown>>(action.method, body).subscribe({
118
122
  next: (resp) => {
119
- // A 'redirect' action POSTs then follows the returned {url}
120
- // (provider-hosted checkout / portal / any external handoff
121
- // declared as basis table_action metadata — no component).
122
- if (action.kind === 'redirect' && resp?.url) {
123
- window.location.href = resp.url;
123
+ const url = resp?.['url'];
124
+ // R = redirect: POST then follow the returned {url} (hosted checkout / portal).
125
+ if (action.kind === 'R' && typeof url === 'string') {
126
+ window.location.href = url;
124
127
  return;
125
128
  }
129
+ // V = reveal: show the returned secret once (e.g. a freshly minted key).
130
+ if (action.kind === 'V') {
131
+ this.#injector.get(MatDialog).open(RevealDialog, { data: resp ?? {}, width: '480px' });
132
+ }
126
133
  this.onActionSuccess(action, record);
127
134
  },
128
135
  error: (err) => {
@@ -0,0 +1,16 @@
1
+ <h2 mat-dialog-title>Copy now</h2>
2
+ <mat-dialog-content>
3
+ <p>This won't be shown again.</p>
4
+ @for (entry of entries; track entry[0]) {
5
+ <p style="margin:12px 0 2px; font-size:12px; opacity:0.7;">{{ entry[0] }}</p>
6
+ <div style="display:flex; align-items:center; gap:8px;">
7
+ <code style="flex:1; word-break:break-all;">{{ entry[1] }}</code>
8
+ <button matIconButton (click)="copy(entry[1])" [attr.aria-label]="'Copy ' + entry[0]">
9
+ <mat-icon>content_copy</mat-icon>
10
+ </button>
11
+ </div>
12
+ }
13
+ </mat-dialog-content>
14
+ <mat-dialog-actions align="end">
15
+ <button matButton mat-dialog-close>Done</button>
16
+ </mat-dialog-actions>
@@ -0,0 +1,22 @@
1
+ import { ChangeDetectionStrategy, Component, inject } from "@angular/core";
2
+ import { MAT_DIALOG_DATA, MatDialogModule } from "@angular/material/dialog";
3
+ import { MatButtonModule } from "@angular/material/button";
4
+ import { MatIconModule } from "@angular/material/icon";
5
+ import { Clipboard } from "@angular/cdk/clipboard";
6
+
7
+ /** Shows the one-time secret a kind:'V' reveal action returned (e.g. a minted API key), with copy buttons. */
8
+ @Component({
9
+ selector: "sail-reveal-dialog",
10
+ changeDetection: ChangeDetectionStrategy.OnPush,
11
+ imports: [MatDialogModule, MatButtonModule, MatIconModule],
12
+ templateUrl: "./reveal_dialog.html",
13
+ })
14
+ export class RevealDialog {
15
+ private readonly clipboard = inject(Clipboard);
16
+ readonly entries = Object.entries(inject<Record<string, unknown>>(MAT_DIALOG_DATA))
17
+ .filter(([, v]) => typeof v === "string" && v.length > 0) as [string, string][];
18
+
19
+ copy(value: string): void {
20
+ this.clipboard.copy(value);
21
+ }
22
+ }
@@ -83,7 +83,7 @@ export interface TableAction {
83
83
  confirmMessage?: string;
84
84
  authorityObject: string; // uppercased table_name — for canExecute()
85
85
  authorityCheck: string; // uppercased action_name — for canExecute()
86
- kind?: 'post' | 'redirect'; // 'redirect' = POST then follow the returned {url}; default 'post'
86
+ kind?: 'P' | 'R' | 'V'; // P post (default) / R redirect (follow {url}) / V reveal (show returned secret once)
87
87
  }
88
88
 
89
89
  export interface ForeignKey {