@nauticana/sail 0.8.1 → 0.8.4
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/README.md +93 -1
- package/package.json +1 -1
- package/src/component/form/form_field.ts +1 -1
- package/src/component/login/otp_input.ts +7 -1
- package/src/component/table/table_detail.ts +2 -2
- package/src/component/table/table_edit.ts +3 -3
- package/src/component/table/table_list.ts +3 -3
- package/src/component/table/table_search.ts +3 -3
- package/src/model/auth.ts +4 -0
package/README.md
CHANGED
|
@@ -1051,6 +1051,25 @@ protected onActionSuccess(action: TableAction, record?: Record<string, unknown>)
|
|
|
1051
1051
|
|
|
1052
1052
|
If your downstream component extends `BaseTable` / `BaseView` and uses the alias-input pattern from v0.8.0 (`input + effect`), you can optionally migrate to `linkedSignal` — but the old pattern still compiles.
|
|
1053
1053
|
|
|
1054
|
+
```typescript
|
|
1055
|
+
import { input, linkedSignal } from '@angular/core';
|
|
1056
|
+
|
|
1057
|
+
export class MyView extends BaseView {
|
|
1058
|
+
// Aliased input MUST be public — Angular's strict template type-checker
|
|
1059
|
+
// resolves `[tableName]="x"` from a parent template back to this field
|
|
1060
|
+
// by name, and a `private` / `protected` modifier rejects the binding
|
|
1061
|
+
// with TS2445. The override signal can be any visibility.
|
|
1062
|
+
readonly tableNameInput = input('', { alias: 'tableName' });
|
|
1063
|
+
|
|
1064
|
+
override readonly tableName = linkedSignal<string, string>({
|
|
1065
|
+
source: () => this.tableNameInput(),
|
|
1066
|
+
computation: (v, prev) => v || prev?.value || '', // preserve .set() when input goes empty
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
```
|
|
1070
|
+
|
|
1071
|
+
The `computation` callback runs whenever the source changes. Returning `v || prev?.value` gives "use the bound value if provided, else keep the last non-empty value we had locally" — same semantics the v0.8.0 `effect()` carried.
|
|
1072
|
+
|
|
1054
1073
|
### 5. Internal: `BaseTable.caption` is no longer memoised; `Navigation.allMenuItems` via `toSignal`
|
|
1055
1074
|
|
|
1056
1075
|
`getCaption()` and `colCaption()` previously cached on first read, so `tableName` updates after first render returned stale captions. They now recompute each call (cost is negligible). `Navigation` reads `allMenuItems` via `toSignal(getMenus(), { initialValue: [] })` instead of a manual `subscribe` in `ngOnInit`.
|
|
@@ -1077,11 +1096,84 @@ Expected errors after the upgrade:
|
|
|
1077
1096
|
- **`'app-*' is not a known element`** — selector rename, see step 1.
|
|
1078
1097
|
- **`'@Output' is deprecated`** — leftover from the v0.8.0 migration; finish it.
|
|
1079
1098
|
- **`peerinvalid` / `--legacy-peer-deps` prompts on `npm install`** — your project still has TS 6.x. Downgrade per step 6.
|
|
1099
|
+
- **`TS2445: Property 'xInput' is protected and only accessible within class …`** during `ng build` from a downstream project, pointing at sail's own `*.html` files (e.g. `[tableName]="..."` in `table_edit.html`) — upgrade to sail `v0.8.1` patch (published after the initial v0.8.1 cut) where the aliased input fields are public. The cause: Angular's strict template type-checker resolves the alias back to the declared field, so `input('', { alias: 'tableName' })` declared as `protected readonly tableNameInput` is unreachable from the parent template that binds `[tableName]=…`. The fix in your own subclasses, if you copied the v0.8.1 pattern: drop the `protected` / `private` modifier on any aliased `input()` field, leave it `readonly`.
|
|
1080
1100
|
|
|
1081
1101
|
### 8. Backend alignment
|
|
1082
1102
|
|
|
1083
1103
|
v0.8.1 targets **keel v0.8.3**. The shared keel API surface didn't grow in this release; the version pin tracks keel's matching v0.8.x line. See [keel/README.md → Migration Guide](https://github.com/nauticana/keel/blob/main/README.md) for the matching backend changes.
|
|
1084
1104
|
|
|
1105
|
+
## Migrating to v0.8.2 — server-driven OTP resend cooldown
|
|
1106
|
+
|
|
1107
|
+
Additive change. v0.8.1 consumers keep compiling.
|
|
1108
|
+
|
|
1109
|
+
### What changed
|
|
1110
|
+
|
|
1111
|
+
`OtpResponse` (model/auth.ts) gained an optional `resendCountdownSec?: number` field. keel v0.8.4 populates it from `--otp_ttl_seconds` so the resend timer in `OtpInputComponent` can match the OTP code's actual server-side lifetime instead of relying on the client-side fallback (still 30s for back-compat with older keel deployments / dev mocks that don't return the field).
|
|
1112
|
+
|
|
1113
|
+
```ts
|
|
1114
|
+
export interface OtpResponse {
|
|
1115
|
+
otpToken: string;
|
|
1116
|
+
resendCountdownSec?: number; // NEW — present when keel >= v0.8.4
|
|
1117
|
+
}
|
|
1118
|
+
```
|
|
1119
|
+
|
|
1120
|
+
`OtpInputComponent.resendCountdownSec` (input, default 30) is **unchanged**. The recommended pattern is for the page that calls `sendOtp` to capture `res.resendCountdownSec` from the response and pass it to the input so the timer reflects server intent.
|
|
1121
|
+
|
|
1122
|
+
### Adoption
|
|
1123
|
+
|
|
1124
|
+
Two-step plumbing on the page that owns the OTP screen:
|
|
1125
|
+
|
|
1126
|
+
1. **Capture from the send response.** After `auth.sendOtp({...}).subscribe(...)`, store `res.resendCountdownSec` in component state and forward it via router state when navigating to the confirm screen.
|
|
1127
|
+
|
|
1128
|
+
```ts
|
|
1129
|
+
this.auth.sendOtp({ contact, contactType, purpose: 'login' }).subscribe({
|
|
1130
|
+
next: (res) => {
|
|
1131
|
+
this.router.navigate(['/login/confirm'], {
|
|
1132
|
+
state: {
|
|
1133
|
+
otpToken: res.otpToken,
|
|
1134
|
+
contact,
|
|
1135
|
+
resendCountdownSec: res.resendCountdownSec, // forward server value
|
|
1136
|
+
},
|
|
1137
|
+
});
|
|
1138
|
+
},
|
|
1139
|
+
});
|
|
1140
|
+
```
|
|
1141
|
+
|
|
1142
|
+
2. **Read on the confirm screen + bind to the input.** Default a local signal to 30 (the legacy fallback) so older keels and the dev OTP mock keep working, then overwrite from router state and from `onResend` responses.
|
|
1143
|
+
|
|
1144
|
+
```ts
|
|
1145
|
+
readonly resendCountdownSec = signal(30);
|
|
1146
|
+
|
|
1147
|
+
constructor() {
|
|
1148
|
+
const state = this.router.getCurrentNavigation()?.extras.state;
|
|
1149
|
+
if (state && typeof state['resendCountdownSec'] === 'number') {
|
|
1150
|
+
this.resendCountdownSec.set(state['resendCountdownSec']);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
onResend() {
|
|
1155
|
+
this.auth.sendOtp({...}).subscribe({
|
|
1156
|
+
next: (res) => {
|
|
1157
|
+
if (typeof res.resendCountdownSec === 'number') {
|
|
1158
|
+
this.resendCountdownSec.set(res.resendCountdownSec);
|
|
1159
|
+
}
|
|
1160
|
+
},
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
```
|
|
1164
|
+
|
|
1165
|
+
```html
|
|
1166
|
+
<sail-otp-input
|
|
1167
|
+
[contact]="contact()"
|
|
1168
|
+
[resendCountdownSec]="resendCountdownSec()"
|
|
1169
|
+
(codeComplete)="verifyOtp($event)"
|
|
1170
|
+
(resend)="onResend()" />
|
|
1171
|
+
```
|
|
1172
|
+
|
|
1173
|
+
### Backend alignment
|
|
1174
|
+
|
|
1175
|
+
v0.8.2 targets **keel v0.8.4**. The new `resendCountdownSec` response field is documented in [keel/README.md → Migration Guide (v0.8.4)](https://github.com/nauticana/keel/blob/main/README.md). Sail will tolerate older keels that don't return the field — the input falls back to its 30s default.
|
|
1176
|
+
|
|
1085
1177
|
## Modernization items
|
|
1086
1178
|
|
|
1087
1179
|
When you extend sail in your own app, apply the same patterns sail itself follows. Downstream projects that copied templates or scaffolding from earlier sail versions will benefit from the same sweep. This list is a checklist — none are sail-specific.
|
|
@@ -1097,7 +1189,7 @@ When you extend sail in your own app, apply the same patterns sail itself follow
|
|
|
1097
1189
|
|
|
1098
1190
|
### Components and directives
|
|
1099
1191
|
|
|
1100
|
-
- **Signal-based inputs/outputs.** `input()` / `output()` instead of `@Input()` / `@Output()`. Read as `this.x()` / `{{ x() }}`.
|
|
1192
|
+
- **Signal-based inputs/outputs.** `input()` / `output()` instead of `@Input()` / `@Output()`. Read as `this.x()` / `{{ x() }}`. **Keep `input()` / `output()` fields publicly accessible** (`readonly` without `private`/`protected`). Angular's strict template type-checker resolves an `{ alias: 'foo' }` back to the declared field name and rejects parent bindings against a non-public field with TS2445.
|
|
1101
1193
|
- **`model()` for two-way bindings.** When the parent needs `[(value)]` binding semantics, declare `value = model('')` rather than rolling your own `@Output() valueChange`.
|
|
1102
1194
|
- **`inject()` for dependencies.** No constructor parameter injection. Field-level `inject(Token)` keeps the constructor zero-arg and lets the class be subclassed without parameter forwarding.
|
|
1103
1195
|
- **`ChangeDetectionStrategy.OnPush` on every component.** Signal-based code is fully compatible with OnPush; without it, change detection still runs needlessly.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nauticana/sail",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.4",
|
|
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
|
"files": [
|
|
@@ -31,7 +31,7 @@ import { TableLookup } from "../table/table_lookup";
|
|
|
31
31
|
]
|
|
32
32
|
})
|
|
33
33
|
export class DynamicField extends BaseTable {
|
|
34
|
-
|
|
34
|
+
readonly tableNameInput = input('', { alias: 'tableName' });
|
|
35
35
|
readonly value = input<any>(undefined);
|
|
36
36
|
readonly valueChange = output<any>();
|
|
37
37
|
readonly recordUpdate = output<{[key: string]: any}>();
|
|
@@ -24,7 +24,13 @@ export class OtpInputComponent implements OnInit {
|
|
|
24
24
|
|
|
25
25
|
readonly length = input(6);
|
|
26
26
|
readonly contact = input('');
|
|
27
|
-
|
|
27
|
+
// Fallback only. Consumers should pass the value returned by
|
|
28
|
+
// /public/otp/send in `resendCountdownSec` on the response — that is
|
|
29
|
+
// the authoritative value (sourced from the keel server's
|
|
30
|
+
// --otp_token_ttl_seconds flag and matches the lifetime of the OTP
|
|
31
|
+
// code itself). 300s is the legacy default, kept so existing callers
|
|
32
|
+
// without the new field don't break.
|
|
33
|
+
readonly resendCountdownSec = input(300);
|
|
28
34
|
readonly disabled = input(false);
|
|
29
35
|
|
|
30
36
|
readonly codeComplete = output<string>();
|
|
@@ -20,8 +20,8 @@ import { OpCode } from "../../model/common";
|
|
|
20
20
|
],
|
|
21
21
|
})
|
|
22
22
|
export class TableDetail extends BaseForm implements OnInit {
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
readonly tableNameInput = input('', { alias: 'tableName' });
|
|
24
|
+
readonly recordsInput = input<any[]>([], { alias: 'records' });
|
|
25
25
|
readonly parentTableName = input('');
|
|
26
26
|
readonly parentRecord = input<any>({});
|
|
27
27
|
/** When true, the parent view is in read-only mode — disable all child row editing. */
|
|
@@ -22,9 +22,9 @@ import { OpCode } from "../../model/common";
|
|
|
22
22
|
]
|
|
23
23
|
})
|
|
24
24
|
export class TableEdit extends BaseForm implements OnInit {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
readonly recordInput = input<any>({}, { alias: 'record' });
|
|
26
|
+
readonly tableNameInput = input('', { alias: 'tableName' });
|
|
27
|
+
readonly apiNameInput = input('', { alias: 'apiName' });
|
|
28
28
|
|
|
29
29
|
readonly record = linkedSignal<any, any>({ source: () => this.recordInput(), computation: (v, p) => v ?? p?.value ?? {} });
|
|
30
30
|
override readonly tableName = linkedSignal<string, string>({ source: () => this.tableNameInput(), computation: (v, p) => v || p?.value || '' });
|
|
@@ -16,9 +16,9 @@ import { MatIconModule } from "@angular/material/icon";
|
|
|
16
16
|
})
|
|
17
17
|
export class TableList extends BaseView implements OnInit {
|
|
18
18
|
override dialogWidth = '400px';
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
readonly tableNameInput = input('', { alias: 'tableName' });
|
|
20
|
+
readonly apiNameInput = input('', { alias: 'apiName' });
|
|
21
|
+
readonly dialogComponentInput = input<any>(undefined, { alias: 'dialogComponent' });
|
|
22
22
|
|
|
23
23
|
// linkedSignal: defaults to the consumer's input, but ngOnInit / route data
|
|
24
24
|
// can override with `.set()`. The `computation` preserves the last
|
|
@@ -18,9 +18,9 @@ import { ApplicationMenu } from "../../model/common";
|
|
|
18
18
|
],
|
|
19
19
|
})
|
|
20
20
|
export class TableSearch extends BaseForm implements OnInit {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
readonly tableNameInput = input('', { alias: 'tableName' });
|
|
22
|
+
readonly apiNameInput = input('', { alias: 'apiName' });
|
|
23
|
+
readonly targetRouteInput = input('', { alias: 'targetRoute' });
|
|
24
24
|
|
|
25
25
|
override readonly tableName = linkedSignal<string, string>({ source: () => this.tableNameInput(), computation: (v, p) => v || p?.value || '' });
|
|
26
26
|
readonly apiName = linkedSignal<string, string>({ source: () => this.apiNameInput(), computation: (v, p) => v || p?.value || '' });
|
package/src/model/auth.ts
CHANGED
|
@@ -80,6 +80,10 @@ export interface OtpRequest {
|
|
|
80
80
|
// wrong code.
|
|
81
81
|
export interface OtpResponse {
|
|
82
82
|
otpToken: string;
|
|
83
|
+
// Seconds the client should wait before allowing a resend; mirrors
|
|
84
|
+
// the server's --otp_ttl_seconds. Optional for back-compat with
|
|
85
|
+
// older keel versions that didn't return it.
|
|
86
|
+
resendCountdownSec?: number;
|
|
83
87
|
}
|
|
84
88
|
|
|
85
89
|
export interface OtpVerifyRequest {
|