@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.
|
|
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<
|
|
121
|
+
this.backendService.executeAction<Record<string, unknown>>(action.method, body).subscribe({
|
|
118
122
|
next: (resp) => {
|
|
119
|
-
|
|
120
|
-
// (
|
|
121
|
-
|
|
122
|
-
|
|
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
|
+
}
|
package/src/model/appdata.ts
CHANGED
|
@@ -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?: '
|
|
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 {
|