@nauticana/sail 0.7.0 → 0.8.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/README.md +419 -15
- package/package.json +2 -2
- package/src/component/abstract/base_async.ts +8 -2
- package/src/component/abstract/base_table.ts +108 -49
- package/src/component/abstract/base_view.ts +32 -45
- package/src/component/billing/checkout_button.html +1 -1
- package/src/component/billing/checkout_button.ts +1 -1
- package/src/component/billing/payment_methods.ts +1 -1
- package/src/component/billing/plan_selector.html +1 -1
- package/src/component/billing/plan_selector.ts +1 -1
- package/src/component/form/form_field.html +1 -1
- package/src/component/form/form_field.ts +8 -7
- package/src/component/form/form_record.html +2 -2
- package/src/component/form/form_record.ts +1 -1
- package/src/component/form/form_table.html +4 -4
- package/src/component/form/form_table.ts +2 -2
- package/src/component/login/account_deletion.html +1 -1
- package/src/component/login/account_deletion.ts +5 -2
- package/src/component/login/chpass_component.html +1 -1
- package/src/component/login/chpass_component.ts +11 -16
- package/src/component/login/confirm_chpass_component.html +1 -1
- package/src/component/login/confirm_chpass_component.ts +9 -15
- package/src/component/login/confirm_register_component.html +1 -1
- package/src/component/login/confirm_register_component.ts +9 -15
- package/src/component/login/consent_gate.ts +1 -1
- package/src/component/login/login_component.html +2 -2
- package/src/component/login/login_component.ts +1 -1
- package/src/component/login/otp_input.html +3 -3
- package/src/component/login/otp_input.ts +1 -1
- package/src/component/login/register_component.html +1 -1
- package/src/component/login/register_component.ts +1 -1
- package/src/component/login/social_login.ts +1 -1
- package/src/component/navigation/navigation.html +3 -3
- package/src/component/navigation/navigation.ts +10 -11
- package/src/component/payment/user_payment_methods.html +46 -0
- package/src/component/payment/user_payment_methods.ts +10 -58
- package/src/component/payout/bank_info_form.html +59 -0
- package/src/component/payout/bank_info_form.ts +21 -77
- package/src/component/payout/provider_onboarding.html +70 -0
- package/src/component/payout/provider_onboarding.ts +9 -80
- package/src/component/security/trusted_devices.html +4 -4
- package/src/component/security/trusted_devices.ts +1 -1
- package/src/component/security/twofactor_setup.html +4 -4
- package/src/component/security/twofactor_setup.ts +18 -26
- package/src/component/security/twofactor_verify.html +1 -1
- package/src/component/security/twofactor_verify.ts +10 -12
- package/src/component/table/table_detail.html +12 -12
- package/src/component/table/table_detail.ts +28 -33
- package/src/component/table/table_edit.html +20 -10
- package/src/component/table/table_edit.ts +29 -18
- package/src/component/table/table_list.html +5 -5
- package/src/component/table/table_list.ts +24 -48
- package/src/component/table/table_lookup.html +4 -4
- package/src/component/table/table_lookup.ts +11 -12
- package/src/component/table/table_report.html +1 -1
- package/src/component/table/table_report.ts +11 -14
- package/src/component/table/table_search.html +5 -5
- package/src/component/table/table_search.ts +31 -42
- package/src/index.ts +2 -0
- package/src/model/appdata.ts +2 -2
- package/src/model/common.ts +39 -1
- package/src/service/auth.service.ts +26 -26
- package/src/service/base_rest.service.ts +20 -0
- package/src/service/billing.service.ts +3 -9
- package/src/service/payout.service.ts +3 -9
- package/src/service/rest_service.ts +5 -5
- package/src/service/user_payment_method.service.ts +4 -8
- package/src/util/text.ts +19 -0
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A shared Angular component library for building CRUD-based admin frontends. Provides table management, form handling, navigation, authentication, two-factor authentication, and trusted device management — all driven by metadata from a [keel](https://github.com/nauticana/keel) Go backend.
|
|
4
4
|
|
|
5
|
-
> **Compatibility:** sail **v0.5.x**
|
|
5
|
+
> **Compatibility:** sail and keel are versioned in lock-step. Use **sail v0.5.x ↔ keel v0.5.x**, **sail v0.6.x / v0.7.x ↔ keel v0.7.x**, **sail v0.8.x ↔ keel v0.8.x**. Newer sail releases extend the contract — older keel servers reject unknown endpoints with HTTP 404 / 400. The v0.8.x line additionally ships the `table_action` framework (per-table custom buttons surfaced in `TableList` / `TableSearch` / `TableEdit` / `TableDetail`); see the [Migrating to v0.7.0 §5 — TableAction](#migrating-to-v070--payout-user-payment-methods-table-actions) section for the seed shape (basis `table_action` + `authorization_object` + `authorization_object_action` rows) and the [keel/README Table Actions](https://github.com/nauticana/keel#table-actions) section for backend wiring via `handler.WrapTableAction`.
|
|
6
6
|
|
|
7
7
|
## What it provides
|
|
8
8
|
|
|
@@ -143,7 +143,7 @@ import { Navigation } from '@nauticana/sail';
|
|
|
143
143
|
selector: 'app-root',
|
|
144
144
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
145
145
|
imports: [Navigation],
|
|
146
|
-
template: `<
|
|
146
|
+
template: `<sail-navigation><span toolbar-title>My App</span></sail-navigation>`,
|
|
147
147
|
})
|
|
148
148
|
export class App {}
|
|
149
149
|
```
|
|
@@ -328,23 +328,23 @@ export class PricingPage {
|
|
|
328
328
|
|
|
329
329
|
**Plan picker** — pure presentational, no API calls:
|
|
330
330
|
```html
|
|
331
|
-
<
|
|
331
|
+
<sail-plan-selector
|
|
332
332
|
[plans]="plans()"
|
|
333
333
|
[selected]="selectedPlan()"
|
|
334
334
|
[features]="{ PRO: ['Unlimited users', '24/7 support'] }"
|
|
335
335
|
(selectionChange)="selectedPlan.set($event)">
|
|
336
|
-
</
|
|
336
|
+
</sail-plan-selector>
|
|
337
337
|
```
|
|
338
338
|
|
|
339
339
|
**Checkout button** — calls `createCheckout()` and redirects to the provider-hosted URL. `PublicPlan.priceId` lets you wire the picker directly to checkout without a local mapping table:
|
|
340
340
|
```html
|
|
341
|
-
<
|
|
341
|
+
<sail-checkout-button
|
|
342
342
|
[priceId]="selectedPlan().priceId!"
|
|
343
343
|
[mode]="'subscription'"
|
|
344
344
|
[successUrl]="'https://app.example.com/billing/done'"
|
|
345
345
|
[cancelUrl]="'https://app.example.com/billing'"
|
|
346
346
|
[email]="userEmail">
|
|
347
|
-
</
|
|
347
|
+
</sail-checkout-button>
|
|
348
348
|
```
|
|
349
349
|
|
|
350
350
|
For one-off charges use `[mode]="'payment'"`. For "save a card without charging" (Stripe SetupIntent), use `[mode]="'setup'"` and **omit** `[priceId]` — keel rejects a non-empty `priceId` in setup mode with 400.
|
|
@@ -364,7 +364,7 @@ metadata: {
|
|
|
364
364
|
|
|
365
365
|
**Payment methods** — lists saved methods with loading and empty states:
|
|
366
366
|
```html
|
|
367
|
-
<
|
|
367
|
+
<sail-payment-methods></sail-payment-methods>
|
|
368
368
|
```
|
|
369
369
|
|
|
370
370
|
### Registration → checkout flow
|
|
@@ -430,12 +430,12 @@ import { ConsentGateComponent, ConsentOption, ConsentState, ConsentType } from '
|
|
|
430
430
|
@Component({
|
|
431
431
|
imports: [ConsentGateComponent],
|
|
432
432
|
template: `
|
|
433
|
-
<
|
|
433
|
+
<sail-consent-gate
|
|
434
434
|
[policyVersion]="'v1'"
|
|
435
435
|
[policyLanguage]="'en'"
|
|
436
436
|
[optionalConsents]="extras"
|
|
437
437
|
(consentStateChange)="onConsent($event)">
|
|
438
|
-
</
|
|
438
|
+
</sail-consent-gate>
|
|
439
439
|
`,
|
|
440
440
|
})
|
|
441
441
|
export class SignupPage {
|
|
@@ -468,12 +468,12 @@ import { BaseAuthService, OtpInputComponent } from '@nauticana/sail';
|
|
|
468
468
|
@Component({
|
|
469
469
|
imports: [OtpInputComponent],
|
|
470
470
|
template: `
|
|
471
|
-
<
|
|
471
|
+
<sail-otp-input
|
|
472
472
|
[contact]="contact()"
|
|
473
473
|
[length]="6"
|
|
474
474
|
(codeComplete)="onVerify($event)"
|
|
475
475
|
(resend)="onResend()">
|
|
476
|
-
</
|
|
476
|
+
</sail-otp-input>
|
|
477
477
|
@if (error()) { <div class="auth-error">{{ error() }}</div> }
|
|
478
478
|
`,
|
|
479
479
|
})
|
|
@@ -510,12 +510,12 @@ Override `verifyOtp()` in your own `AuthService extends BaseAuthService` when yo
|
|
|
510
510
|
`SocialLoginComponent` renders Google / Apple buttons using each provider's official SDK. It loads the SDKs dynamically via `loadScript()` — nothing is bundled into your app.
|
|
511
511
|
|
|
512
512
|
```html
|
|
513
|
-
<
|
|
513
|
+
<sail-social-login
|
|
514
514
|
[providers]="['google', 'apple']"
|
|
515
515
|
[consent]="consentState"
|
|
516
516
|
(loginSuccess)="onSuccess($event)"
|
|
517
517
|
(loginError)="onError($event)">
|
|
518
|
-
</
|
|
518
|
+
</sail-social-login>
|
|
519
519
|
```
|
|
520
520
|
|
|
521
521
|
Config required in `SAIL_GUI_CONFIG`:
|
|
@@ -557,7 +557,7 @@ auth.deleteAccount(reauth, 'Closing my account.').subscribe();
|
|
|
557
557
|
### Account deletion
|
|
558
558
|
|
|
559
559
|
```html
|
|
560
|
-
<
|
|
560
|
+
<sail-account-deletion [confirmationText]="'DELETE'"></sail-account-deletion>
|
|
561
561
|
```
|
|
562
562
|
|
|
563
563
|
Typed-confirmation UX: the destructive button is disabled until the user types the exact `confirmationText` (default `DELETE`) **and** confirms their password. Optional reason textarea is forwarded to the backend. On success, local session is cleared and the router navigates to `config.accountDeletedRoute` (default `/login/local`).
|
|
@@ -567,7 +567,7 @@ Typed-confirmation UX: the destructive button is disabled until the user types t
|
|
|
567
567
|
`TrustedDevicesComponent` ships a "Sign out of all devices" button that reveals a password field, then calls `BaseAuthService.logoutEverywhere({ password })` (`POST /api/user/logout-everywhere`). It also displays a single-device-mode banner when configured:
|
|
568
568
|
|
|
569
569
|
```html
|
|
570
|
-
<
|
|
570
|
+
<sail-trusted-devices [singleDeviceSession]="user.singleDeviceSession"></sail-trusted-devices>
|
|
571
571
|
```
|
|
572
572
|
|
|
573
573
|
Pass `singleDeviceSession` from your login response (the field is part of `LoginResponse2FA`). When true, the banner reads: *"This account is in single-device mode — signing in on another device will sign you out here."*
|
|
@@ -739,3 +739,407 @@ Type errors after upgrade fall into two buckets:
|
|
|
739
739
|
### 8. Backend alignment
|
|
740
740
|
|
|
741
741
|
This library only talks to keel **v0.5.x**. Older keel servers will reject the `otpToken` field on verify with HTTP 400. Upgrade keel and sail together. See [keel/README.md → Migration Guide](https://github.com/nauticana/keel/blob/main/README.md) for the matching backend changes.
|
|
742
|
+
|
|
743
|
+
## Migrating to v0.7.0 — payout, user payment methods, table actions
|
|
744
|
+
|
|
745
|
+
The v0.6 / v0.7 line introduced three additive feature groups against keel v0.7.x. All net-new exports; no breaking changes from v0.5.x. Pull what you need.
|
|
746
|
+
|
|
747
|
+
| Version | Surface |
|
|
748
|
+
|---|---|
|
|
749
|
+
| **v0.6.0** | Payout onboarding (KYC) + multi-partner account reuse |
|
|
750
|
+
| **v0.6.1** | End-user saved payment methods (cards / wallets) |
|
|
751
|
+
| **v0.7.0** | `TableAction` — backend-defined custom buttons on table screens |
|
|
752
|
+
|
|
753
|
+
### 1. New exports
|
|
754
|
+
|
|
755
|
+
| From | Export | Purpose |
|
|
756
|
+
|---|---|---|
|
|
757
|
+
| `@nauticana/sail` | `PayoutService` | keel/payout API client — hosted-KYC launch, reuse flow, status |
|
|
758
|
+
| `@nauticana/sail` | `PayoutProviderOnboardingComponent` (`<sail-payout-provider-onboarding>`) | Drop-in onboarding step with reuse picker + hosted-KYC launcher |
|
|
759
|
+
| `@nauticana/sail` | `PayoutBankInfoFormComponent` (`<sail-payout-bank-info-form>`) | Tax + payout details form (country / currency / tax ID / billing address / agreement) |
|
|
760
|
+
| `@nauticana/sail` | `UserPaymentMethodService` | Saved-card list / delete / set-default API client |
|
|
761
|
+
| `@nauticana/sail` | `UserPaymentMethodsComponent` (`<sail-user-payment-methods>`) | List of saved cards/wallets with set-default + delete |
|
|
762
|
+
| `@nauticana/sail` | `TableAction`, `ReusableAccount`, `PayoutOnboardingSession`, `BankInfoFormValue`, `CountryProfile`, `UserPaymentMethod`, `DEFAULT_COUNTRY_PROFILES` | Model types / defaults |
|
|
763
|
+
|
|
764
|
+
### 2. New keel endpoints
|
|
765
|
+
|
|
766
|
+
Make sure your keel deployment exposes these — they're all under `/api/v1/`:
|
|
767
|
+
|
|
768
|
+
| Endpoint | Method | Purpose |
|
|
769
|
+
|---|---|---|
|
|
770
|
+
| `/api/v1/payout/onboard/start` | POST | Open hosted-KYC; returns `{ url, externalAccountId, expiresAt }` |
|
|
771
|
+
| `/api/v1/payout/reusable` | POST | List provider accounts the user has on other partners |
|
|
772
|
+
| `/api/v1/payout/reusable/link` | POST | Copy a `providerAccountId` onto the active partner's `user_bank_info` row |
|
|
773
|
+
| `/api/v1/payout/status` | POST | `{ complete: true }` once the active partner's row has a `providerAccountId` |
|
|
774
|
+
| `/api/v1/payment-methods/set-default` | POST | Atomic multi-row UPDATE — sets one row as default, clears the rest |
|
|
775
|
+
| `/api/v1/{table}/{action_name}` | POST | Per-table custom actions resolved from `basis.table_action` |
|
|
776
|
+
|
|
777
|
+
`list` / `delete` for the `user_payment_method` table go through keel's generic REST CRUD (`/api/v1/user_payment_method/list|delete`) — `user_payment_method` is a UserSpecific basis table, so keel auto-scopes reads to the caller and owner-locks DELETE. No custom endpoint needed for those two.
|
|
778
|
+
|
|
779
|
+
### 3. Payout onboarding wiring
|
|
780
|
+
|
|
781
|
+
Drop the two components into a wizard step. Routing between them stays in the consumer app — sail emits events, doesn't navigate.
|
|
782
|
+
|
|
783
|
+
```html
|
|
784
|
+
@switch (step()) {
|
|
785
|
+
@case ('bank') {
|
|
786
|
+
<sail-payout-bank-info-form
|
|
787
|
+
(submitted)="onBankInfo($event)"
|
|
788
|
+
(back)="step.set('plan')">
|
|
789
|
+
</sail-payout-bank-info-form>
|
|
790
|
+
}
|
|
791
|
+
@case ('provider') {
|
|
792
|
+
<sail-payout-provider-onboarding
|
|
793
|
+
(linked)="step.set('done')"
|
|
794
|
+
(started)="step.set('waiting')"
|
|
795
|
+
(skipped)="step.set('done')"
|
|
796
|
+
(back)="step.set('bank')">
|
|
797
|
+
</sail-payout-provider-onboarding>
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
```
|
|
801
|
+
|
|
802
|
+
`BankInfoFormValue` maps 1:1 to `basis.user_bank_info` columns. The consumer decides where to POST it — typically a direct generic-CRUD insert against `/api/v1/user_bank_info`. The provider account itself is created by `<sail-payout-provider-onboarding>` via `PayoutService.startOnboarding()` → hosted KYC → webhook back to keel.
|
|
803
|
+
|
|
804
|
+
For apps that operate on a single keel partner, the reuse picker stays empty and only the "Start onboarding" CTA renders. Multi-partner apps get the picker for free.
|
|
805
|
+
|
|
806
|
+
### 4. Saved payment methods wiring
|
|
807
|
+
|
|
808
|
+
```html
|
|
809
|
+
<sail-user-payment-methods
|
|
810
|
+
[title]="'Payment Methods'"
|
|
811
|
+
(addClicked)="goToSetupIntent()"
|
|
812
|
+
(defaultChanged)="onDefaultChanged($event)"
|
|
813
|
+
(deleted)="onDeleted($event)">
|
|
814
|
+
</sail-user-payment-methods>
|
|
815
|
+
```
|
|
816
|
+
|
|
817
|
+
`(addClicked)` is the only event you must wire — sail intentionally does **not** ship a SetupIntent UI (providers differ too much; consumer flows them through Stripe Elements / Apple Pay / Google Pay / etc.). Listen for the event and route to your own SetupIntent screen.
|
|
818
|
+
|
|
819
|
+
### 5. TableAction — backend-defined per-table actions
|
|
820
|
+
|
|
821
|
+
v0.7.0 surfaces a new `TableAction` channel: keel's REST engine ships per-table action buttons from `basis.table_action`. The shipped `TableList`, `TableSearch`, `TableEdit`, and `TableDetail` templates render these automatically (toolbar for table-level actions, per-row icon buttons for record-specific ones). Authorization gating mirrors `canRead`/`canCreate` etc. — `canExecuteAction(action)` is checked against `(authorityObject, authorityCheck, table_name)`.
|
|
822
|
+
|
|
823
|
+
For your own components that subclass `BaseView` / `BaseForm`, expose the same buttons in your templates:
|
|
824
|
+
|
|
825
|
+
```html
|
|
826
|
+
@for (action of getActions(false); track action.action) {
|
|
827
|
+
@if (canExecuteAction(action)) {
|
|
828
|
+
<button matButton="outlined" type="button"
|
|
829
|
+
[title]="action.caption"
|
|
830
|
+
(click)="executeAction(action)">
|
|
831
|
+
@if (action.icon) { <mat-icon>{{ action.icon }}</mat-icon> }
|
|
832
|
+
{{ action.caption }}
|
|
833
|
+
</button>
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
@for (action of getActions(true); track action.action) {
|
|
838
|
+
@if (canExecuteAction(action)) {
|
|
839
|
+
<button matIconButton type="button"
|
|
840
|
+
[title]="action.caption"
|
|
841
|
+
(click)="executeAction(action, record)">
|
|
842
|
+
<mat-icon>{{ action.icon || 'play_arrow' }}</mat-icon>
|
|
843
|
+
</button>
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
```
|
|
847
|
+
|
|
848
|
+
`executeAction()` is provided by sail's table components (it POSTs the row's primary-key columns to `action.method`). If you wrote your own subclass and want the same behaviour, inject `BackendService` and call `backendService.executeAction(action.method, body)`. The `body` is `{}` for table-level actions and the row PK (via `primaryKeyValues(record)`) for record-specific ones.
|
|
849
|
+
|
|
850
|
+
### 6. Backend alignment
|
|
851
|
+
|
|
852
|
+
v0.6 / v0.7 require keel **v0.7.x**. The earlier `keel v0.5.x` server doesn't expose `/api/v1/payout/*`, `/api/v1/payment-methods/set-default`, or the `basis.table_action` seed data. Upgrade keel and sail together.
|
|
853
|
+
|
|
854
|
+
## Migrating to v0.8.0 — signal-driven modernization
|
|
855
|
+
|
|
856
|
+
v0.8.0 finishes the Angular-signals migration that v0.5–v0.7 started incrementally. Every `@Input()` / `@Output()` decorator, `EventEmitter`, plain-field-on-`BaseTable` access, and legacy Material M2 button selector is gone. Downstream code that extends sail's abstract classes or copies its template patterns will need the steps below — there is no shim.
|
|
857
|
+
|
|
858
|
+
The TS-side core peer requirements bump with this release:
|
|
859
|
+
|
|
860
|
+
| | v0.7.x | v0.8.x |
|
|
861
|
+
|---|---|---|
|
|
862
|
+
| Angular | `^21.0.0` | `^21.2.0` |
|
|
863
|
+
| TypeScript | `~5.9.2` | `~6.0.3` |
|
|
864
|
+
|
|
865
|
+
### 1. `BaseTable.tableName` is now a `WritableSignal<string>`
|
|
866
|
+
|
|
867
|
+
The single highest-impact change. Before:
|
|
868
|
+
|
|
869
|
+
```typescript
|
|
870
|
+
// BaseTable
|
|
871
|
+
tableName = '';
|
|
872
|
+
|
|
873
|
+
// Subclass
|
|
874
|
+
@Input() override tableName = '';
|
|
875
|
+
|
|
876
|
+
// In your code
|
|
877
|
+
if (this.tableName === 'orders') { ... }
|
|
878
|
+
this.tableName = 'orders';
|
|
879
|
+
|
|
880
|
+
// In your template
|
|
881
|
+
[tableName]="tableName"
|
|
882
|
+
{{ tableName }}
|
|
883
|
+
```
|
|
884
|
+
|
|
885
|
+
After:
|
|
886
|
+
|
|
887
|
+
```typescript
|
|
888
|
+
// BaseTable
|
|
889
|
+
readonly tableName: WritableSignal<string> = signal('');
|
|
890
|
+
|
|
891
|
+
// In your code
|
|
892
|
+
if (this.tableName() === 'orders') { ... }
|
|
893
|
+
this.tableName.set('orders');
|
|
894
|
+
|
|
895
|
+
// In your template
|
|
896
|
+
[tableName]="tableName()"
|
|
897
|
+
{{ tableName() }}
|
|
898
|
+
```
|
|
899
|
+
|
|
900
|
+
If your subclass exposes `tableName` as a route/parent input, declare an aliased `input()` and sync it into the inherited signal in the constructor:
|
|
901
|
+
|
|
902
|
+
```typescript
|
|
903
|
+
import { effect, input } from '@angular/core';
|
|
904
|
+
|
|
905
|
+
export class YourTableComponent extends BaseForm {
|
|
906
|
+
readonly tableNameInput = input('', { alias: 'tableName' });
|
|
907
|
+
|
|
908
|
+
constructor() {
|
|
909
|
+
super();
|
|
910
|
+
effect(() => {
|
|
911
|
+
const v = this.tableNameInput();
|
|
912
|
+
if (v) this.tableName.set(v);
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
ngOnInit() {
|
|
917
|
+
// Route-data fallback now uses .set(), not assignment:
|
|
918
|
+
if (!this.tableName() && data['tableName']) this.tableName.set(data['tableName']);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
```
|
|
922
|
+
|
|
923
|
+
### 2. `BaseView.apiName` and `BaseView.dialogComponent` are signals too
|
|
924
|
+
|
|
925
|
+
Same migration path as `tableName`. The TableList/TableLookup pattern in v0.5–v0.7 read these as plain strings and assigned in `ngOnInit`; they are now `WritableSignal<string>` / `WritableSignal<any>`. Update subclass code:
|
|
926
|
+
|
|
927
|
+
```diff
|
|
928
|
+
- if (!this.apiName && data['apiName']) this.apiName = data['apiName'];
|
|
929
|
+
- this.backendService.list<MyRow>(this.apiName, terms).subscribe(...);
|
|
930
|
+
+ if (!this.apiName() && data['apiName']) this.apiName.set(data['apiName']);
|
|
931
|
+
+ this.backendService.list<MyRow>(this.apiName(), terms).subscribe(...);
|
|
932
|
+
```
|
|
933
|
+
|
|
934
|
+
If you bind `[apiName]` / `[dialogComponent]` on a sail subclass from a parent template, declare aliased inputs and sync via `effect()`, exactly like `tableNameInput` above.
|
|
935
|
+
|
|
936
|
+
### 3. `@Input()` / `@Output()` → `input()` / `output()`
|
|
937
|
+
|
|
938
|
+
Every decorator-based input/output across sail is now signal-based. Update your own components to match. The signal-input requires calling the field as a function inside the class and in templates.
|
|
939
|
+
|
|
940
|
+
```diff
|
|
941
|
+
- @Input() title = 'Payments';
|
|
942
|
+
- @Output() saved = new EventEmitter<Payment>();
|
|
943
|
+
+ readonly title = input('Payments');
|
|
944
|
+
+ readonly saved = output<Payment>();
|
|
945
|
+
|
|
946
|
+
// class body
|
|
947
|
+
- this.title // string
|
|
948
|
+
- this.saved.emit(p);
|
|
949
|
+
+ this.title() // string (signal read)
|
|
950
|
+
+ this.saved.emit(p); // unchanged
|
|
951
|
+
|
|
952
|
+
// template
|
|
953
|
+
- {{ title }}
|
|
954
|
+
+ {{ title() }}
|
|
955
|
+
```
|
|
956
|
+
|
|
957
|
+
`EventEmitter` is no longer imported from `@angular/core` in sail; outputs created with `output<T>()` are returned as `OutputEmitterRef<T>` with the same `.emit(v)` API.
|
|
958
|
+
|
|
959
|
+
For inputs whose **parent value can change at runtime** and you also want a local writable copy (the old "default value, then override in `ngOnInit`" pattern), use the alias + effect template above. If you only need the parent value reactively, just call `this.foo()` everywhere.
|
|
960
|
+
|
|
961
|
+
### 4. Inline component templates moved to sibling `.html`
|
|
962
|
+
|
|
963
|
+
`UserPaymentMethodsComponent`, `PayoutBankInfoFormComponent`, and `PayoutProviderOnboardingComponent` no longer ship inline `template:` strings — they reference `templateUrl: './*.html'` in the same folder. The compiled output is identical; downstream consumers that just import the component classes need no change. If you have a fork that edits these templates inline, port your edits into the new `.html` files.
|
|
964
|
+
|
|
965
|
+
### 5. Clean install + recompile
|
|
966
|
+
|
|
967
|
+
```bash
|
|
968
|
+
rm -rf node_modules package-lock.json
|
|
969
|
+
npm install
|
|
970
|
+
ng build
|
|
971
|
+
```
|
|
972
|
+
|
|
973
|
+
Type errors after the upgrade fall into four buckets:
|
|
974
|
+
|
|
975
|
+
- **`This expression is not callable. Type 'String' has no call signatures.`** — a template still reads `tableName` / `apiName` as a plain field. Add `()`. See step 1.
|
|
976
|
+
- **`Cannot assign to 'tableName' because it is a read-only property.`** — a subclass declared `readonly tableName = input('')`, conflicting with the inherited writable signal. Use the `tableNameInput = input('', { alias: 'tableName' })` + `effect()` pattern from step 1 instead.
|
|
977
|
+
- **`Property 'X' does not exist on type 'EventEmitter<T>'.`** — you still import `EventEmitter`. Replace the field with `output<T>()` per step 3.
|
|
978
|
+
- **`'@Input' is deprecated`** / Angular language-service squiggles on decorators — step 3.
|
|
979
|
+
|
|
980
|
+
## Migrating to v0.8.1 — selector standardisation, OOP cleanup, keel v0.8.3 alignment
|
|
981
|
+
|
|
982
|
+
v0.8.1 is a polish pass on top of v0.8.0's signal migration. The biggest change is **the component selector prefix**: every sail component is now `sail-*`, where v0.8.0 still shipped a mix of `app-*` (older components) and `sail-*` (the v0.6 / v0.7 additions). The rest of the release is internal OOP cleanup that's mostly invisible to downstream code, plus a TypeScript downgrade and a keel pin.
|
|
983
|
+
|
|
984
|
+
| | v0.8.0 | v0.8.1 |
|
|
985
|
+
|---|---|---|
|
|
986
|
+
| keel | `v0.8.0` | `v0.8.3` |
|
|
987
|
+
| TypeScript | `~6.0.3` | `~5.9.3` |
|
|
988
|
+
| Component selector prefix | `app-*` (older) + `sail-*` (newer) | `sail-*` (uniform) |
|
|
989
|
+
|
|
990
|
+
### 1. Selector rename — `app-*` → `sail-*` (and `table-*` / `dynamic-field` / `record-form` → `sail-*`)
|
|
991
|
+
|
|
992
|
+
Every sail component selector now starts with `sail-`. **This is the breaking change**: any downstream template that embeds a sail component needs the prefix updated. Search-and-replace across your templates:
|
|
993
|
+
|
|
994
|
+
| Old | New |
|
|
995
|
+
|---|---|
|
|
996
|
+
| `<app-navigation>` | `<sail-navigation>` |
|
|
997
|
+
| `<app-login>` | `<sail-login>` |
|
|
998
|
+
| `<app-register>` | `<sail-register>` |
|
|
999
|
+
| `<app-chpass>` | `<sail-chpass>` |
|
|
1000
|
+
| `<app-confirm-register>` | `<sail-confirm-register>` |
|
|
1001
|
+
| `<app-confirm-chpass>` | `<sail-confirm-chpass>` |
|
|
1002
|
+
| `<app-twofactor-setup>` | `<sail-twofactor-setup>` |
|
|
1003
|
+
| `<app-twofactor-verify>` | `<sail-twofactor-verify>` |
|
|
1004
|
+
| `<app-trusted-devices>` | `<sail-trusted-devices>` |
|
|
1005
|
+
| `<app-account-deletion>` | `<sail-account-deletion>` |
|
|
1006
|
+
| `<app-consent-gate>` | `<sail-consent-gate>` |
|
|
1007
|
+
| `<app-otp-input>` | `<sail-otp-input>` |
|
|
1008
|
+
| `<app-social-login>` | `<sail-social-login>` |
|
|
1009
|
+
| `<app-plan-selector>` | `<sail-plan-selector>` |
|
|
1010
|
+
| `<app-checkout-button>` | `<sail-checkout-button>` |
|
|
1011
|
+
| `<app-payment-methods>` | `<sail-payment-methods>` |
|
|
1012
|
+
| `<table-list>` | `<sail-table-list>` |
|
|
1013
|
+
| `<table-search>` | `<sail-table-search>` |
|
|
1014
|
+
| `<table-edit>` | `<sail-table-edit>` |
|
|
1015
|
+
| `<table-detail>` | `<sail-table-detail>` |
|
|
1016
|
+
| `<table-lookup>` | `<sail-table-lookup>` |
|
|
1017
|
+
| `<table-report>` | `<sail-table-report>` |
|
|
1018
|
+
| `<table-form>` | `<sail-table-form>` |
|
|
1019
|
+
| `<record-form>` | `<sail-record-form>` |
|
|
1020
|
+
| `<dynamic-field>` | `<sail-dynamic-field>` |
|
|
1021
|
+
|
|
1022
|
+
The payout / user-payment-method selectors (`<sail-payout-bank-info-form>`, `<sail-payout-provider-onboarding>`, `<sail-user-payment-methods>`) were already on `sail-*` in v0.7.0 — no change there.
|
|
1023
|
+
|
|
1024
|
+
**Why:** the Angular style guide assigns `app-*` to the consumer application (Angular CLI defaults to it for new components). A library shipping `app-*` selectors collides with the downstream's own components and reads ambiguously in mixed templates. v0.8.1 makes the prefix uniform across sail and unambiguous against your code.
|
|
1025
|
+
|
|
1026
|
+
### 2. Internal: TableAction template-method pull-up
|
|
1027
|
+
|
|
1028
|
+
`executeAction()` was duplicated three times across `TableList`, `TableSearch`, and `TableDetail` in v0.8.0. v0.8.1 moves the body into `BaseTable` with two `protected` hooks:
|
|
1029
|
+
|
|
1030
|
+
```typescript
|
|
1031
|
+
protected beforeExecuteAction(action: TableAction, record?: Record<string, unknown>): boolean { return true; }
|
|
1032
|
+
protected onActionSuccess(action: TableAction, record?: Record<string, unknown>): void { /* no-op */ }
|
|
1033
|
+
```
|
|
1034
|
+
|
|
1035
|
+
**For your subclasses:** if you have a custom view that extends `BaseTable` / `BaseForm` / `BaseView` and renders the action toolbar, you no longer write your own `executeAction()` — inherit it. Override `onActionSuccess()` to refresh your screen (the way `TableList` re-fetches) and `beforeExecuteAction()` to add per-screen pre-flight guards (the way `TableDetail` blocks unsaved rows).
|
|
1036
|
+
|
|
1037
|
+
**For your templates:** unchanged. Action buttons still call `(click)="executeAction(action, record)"`.
|
|
1038
|
+
|
|
1039
|
+
**Bonus:** `TableEdit` now renders per-record actions too (v0.8.0 was missing them) and re-fetches the record after a successful action.
|
|
1040
|
+
|
|
1041
|
+
### 3. Internal: `BaseAsync` pulled up onto 5 more components
|
|
1042
|
+
|
|
1043
|
+
`ChpassComponent`, `ConfirmChpassComponent`, `ConfirmRegisterComponent`, `TwoFactorSetupComponent`, and `TwoFactorVerifyComponent` were re-declaring their own `errorMessage` / `successMessage` signals and hand-rolled subscribe blocks. They now `extends BaseAsync` and use `this.run(obs, onSuccess, fallback)` — public template API is unchanged (`{{ errorMessage() }}` / `{{ successMessage() }}` still work).
|
|
1044
|
+
|
|
1045
|
+
### 4. Internal: `BaseRestService` + `requireAuth()` + `openRecordDialog()` + `linkedSignal` alias-inputs
|
|
1046
|
+
|
|
1047
|
+
- **`BaseRestService`** (`src/service/base_rest.service.ts`) owns the shared `inject(HttpClient)` and `url(path)` helper. `BackendService`, `BaseAuthService`, `BillingService`, `PayoutService`, and `UserPaymentMethodService` all extend it. The 27 hand-spelled `RestURL.httpHost + RestURL.xURL` getters in `BaseAuthService` are now `readonly` fields built via `this.url(...)`.
|
|
1048
|
+
- **`BaseTable.requireAuth(check, verb)`** replaces 8 copies of `alert('Missing authorization to … records')` across the codebase.
|
|
1049
|
+
- **`BaseView.openRecordDialog(record, isNew)`** collapses the duplicated `addRecord` / `editRecord` dialog-open bodies.
|
|
1050
|
+
- **`linkedSignal`** replaces the v0.8.0 `input() + signal + effect()` pattern across `TableList`, `TableSearch`, `TableEdit`, `TableDetail`, and `DynamicField`. One-line declarations per field; behaviour is unchanged (empty input doesn't override a `.set()` value).
|
|
1051
|
+
|
|
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
|
+
|
|
1054
|
+
### 5. Internal: `BaseTable.caption` is no longer memoised; `Navigation.allMenuItems` via `toSignal`
|
|
1055
|
+
|
|
1056
|
+
`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`.
|
|
1057
|
+
|
|
1058
|
+
### 6. TypeScript pinned to `~5.9.3`
|
|
1059
|
+
|
|
1060
|
+
`@angular/compiler-cli@21.2` declares `typescript >=5.9 <6.1` as a peer, but downstream projects report that TS `6.0.3` requires `--legacy-peer-deps` on `npm install` because of transitive package mismatches. To keep the install path clean for all consumers, v0.8.1 pins to `~5.9.3`. Bump your downstream's `package.json` to match:
|
|
1061
|
+
|
|
1062
|
+
```diff
|
|
1063
|
+
- "typescript": "~6.0.x"
|
|
1064
|
+
+ "typescript": "~5.9.3"
|
|
1065
|
+
```
|
|
1066
|
+
|
|
1067
|
+
### 7. Clean install + recompile
|
|
1068
|
+
|
|
1069
|
+
```bash
|
|
1070
|
+
rm -rf node_modules package-lock.json
|
|
1071
|
+
npm install
|
|
1072
|
+
ng build
|
|
1073
|
+
```
|
|
1074
|
+
|
|
1075
|
+
Expected errors after the upgrade:
|
|
1076
|
+
|
|
1077
|
+
- **`'app-*' is not a known element`** — selector rename, see step 1.
|
|
1078
|
+
- **`'@Output' is deprecated`** — leftover from the v0.8.0 migration; finish it.
|
|
1079
|
+
- **`peerinvalid` / `--legacy-peer-deps` prompts on `npm install`** — your project still has TS 6.x. Downgrade per step 6.
|
|
1080
|
+
|
|
1081
|
+
### 8. Backend alignment
|
|
1082
|
+
|
|
1083
|
+
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
|
+
|
|
1085
|
+
## Modernization items
|
|
1086
|
+
|
|
1087
|
+
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.
|
|
1088
|
+
|
|
1089
|
+
### Templates
|
|
1090
|
+
|
|
1091
|
+
- **Native control flow.** Use `@if` / `@for` / `@switch` / `@let` blocks. `*ngIf`, `*ngFor`, `*ngSwitch` are legacy.
|
|
1092
|
+
- **Bindings, not directives, for class / style.** `[class.foo]="cond"`, `[style.width.px]="n"` — don't use `ngClass` / `ngStyle`.
|
|
1093
|
+
- **Material 21 M3 button selectors.** See step 5 above. `mat-raised-button` / `mat-stroked-button` etc. work but are not the current API.
|
|
1094
|
+
- **No `color="primary|accent|warn"` on Material components.** Use the `.primary` / `.accent` / `.warn` global CSS classes.
|
|
1095
|
+
- **Template spread inside expressions** (Angular 21). When you have a TS helper that only exists to merge objects for a binding, fold it into the template: `[config]="{ ...base, label: 'Override' }"`, `[items]="[...defaults, ...extra]"`, `[out]="fn(...args)"`. (Note: only valid inside array literals, object literals, or call expressions — there is no JSX-style host-attribute spread.)
|
|
1096
|
+
- **No structural directive on the same element as a component selector.** Already enforced by native control flow.
|
|
1097
|
+
|
|
1098
|
+
### Components and directives
|
|
1099
|
+
|
|
1100
|
+
- **Signal-based inputs/outputs.** `input()` / `output()` instead of `@Input()` / `@Output()`. Read as `this.x()` / `{{ x() }}`.
|
|
1101
|
+
- **`model()` for two-way bindings.** When the parent needs `[(value)]` binding semantics, declare `value = model('')` rather than rolling your own `@Output() valueChange`.
|
|
1102
|
+
- **`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
|
+
- **`ChangeDetectionStrategy.OnPush` on every component.** Signal-based code is fully compatible with OnPush; without it, change detection still runs needlessly.
|
|
1104
|
+
- **`host: { ... }` metadata, not `@HostBinding` / `@HostListener`.** Same effect, no decorators.
|
|
1105
|
+
- **Omit `standalone: true`.** It's the default in Angular v20+. Setting it explicitly is noise.
|
|
1106
|
+
- **Move HTML and styles to sibling files** when they grow past a few lines (`templateUrl: './*.html'`, `styleUrl: './*.scss'` or `styles: '...'`).
|
|
1107
|
+
|
|
1108
|
+
### State and reactivity
|
|
1109
|
+
|
|
1110
|
+
- **`signal()` for component state**, `computed()` for derived state, `effect()` for side effects that depend on signals. Don't roll your own `BehaviorSubject` for local UI state.
|
|
1111
|
+
- **`linkedSignal()`** for "use this input value by default, but allow local override" patterns.
|
|
1112
|
+
- **`takeUntilDestroyed()`** on long-lived observables (`valueChanges`, `route.queryParams`, `route.paramMap`, custom intervals). One-shot HTTP requests can skip it, but `valueChanges` and route streams must have it. Call it in a constructor or other injection context.
|
|
1113
|
+
- **`toSignal()`** for converting an `Observable<T>` into a signal at component boundaries — useful for `BillingService.listPlans()` and similar one-shots you read in templates.
|
|
1114
|
+
- **No `mutate()` on signals** — it was removed. Use `update(fn)` or `set(newValue)`.
|
|
1115
|
+
|
|
1116
|
+
### Router
|
|
1117
|
+
|
|
1118
|
+
- **Standalone `isActive(url, router, options?)` function** from `@angular/router`. Returns `Signal<boolean>`. `Router.prototype.isActive(...)` was `@deprecated` in Angular 21.1.
|
|
1119
|
+
- **`provideRouter([...])` for bootstrap.** No `RouterModule.forRoot`.
|
|
1120
|
+
- **Component input binding** for route params: enable `withComponentInputBinding()` and declare `id = input.required<string>()` on the routed component — drops the `ActivatedRoute` injection.
|
|
1121
|
+
|
|
1122
|
+
### HTTP
|
|
1123
|
+
|
|
1124
|
+
- **`provideHttpClient(withInterceptors([...]))`.** No `HttpClientModule`.
|
|
1125
|
+
- **Functional interceptors**, not class-based — sail's `authInterceptor` and `apiResponseInterceptor` are already functional; follow the same shape for your own.
|
|
1126
|
+
|
|
1127
|
+
### Forms
|
|
1128
|
+
|
|
1129
|
+
- **Reactive forms over template-driven.** sail uses `FormGroup` / `FormBuilder` throughout.
|
|
1130
|
+
- **`fb.nonNullable.group({...})`** for strict typing. Avoids `string | null` everywhere.
|
|
1131
|
+
|
|
1132
|
+
### Bootstrap
|
|
1133
|
+
|
|
1134
|
+
- **`bootstrapApplication()` with standalone components.** No `@NgModule`, no `AppModule`.
|
|
1135
|
+
- **`provideZonelessChangeDetection()`** — Angular 21's default. sail's components are signal-based and zoneless-safe.
|
|
1136
|
+
|
|
1137
|
+
### Images
|
|
1138
|
+
|
|
1139
|
+
- **`NgOptimizedImage`** for static `<img>` with known dimensions. **Exception:** base64 / data URIs aren't supported by `NgOptimizedImage` — use a plain `<img>` for those (the 2FA QR code in `TwoFactorSetupComponent` is the canonical case).
|
|
1140
|
+
|
|
1141
|
+
### Tooling
|
|
1142
|
+
|
|
1143
|
+
- **TypeScript `~6.0.3`** — within Angular 21.2's `>=5.9 <6.1` peer range. Bump from `~5.9.x`.
|
|
1144
|
+
- **`@angular/*` `^21.2.0`** for the matching set: core, common, compiler, forms, router, cdk, material.
|
|
1145
|
+
- **`npm update`** periodically to pull patch releases within the caret range.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nauticana/sail",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.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
|
"files": [
|
|
@@ -53,6 +53,6 @@
|
|
|
53
53
|
"@angular/router": "^21.2.8",
|
|
54
54
|
"class-validator": "^0.15.1",
|
|
55
55
|
"rxjs": "~7.8.0",
|
|
56
|
-
"typescript": "~5.9.
|
|
56
|
+
"typescript": "~5.9.3"
|
|
57
57
|
}
|
|
58
58
|
}
|
|
@@ -20,9 +20,15 @@ export abstract class BaseAsync {
|
|
|
20
20
|
this.successMessage.set('');
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* Set errorMessage from an HTTP error object; falls back to `fallback`.
|
|
25
|
+
*
|
|
26
|
+
* Reads `err.error.detail` first (keel emits RFC 7807 ProblemDetail with
|
|
27
|
+
* `detail` as the human-readable field), then `err.error.message` for
|
|
28
|
+
* legacy / non-RFC-7807 endpoints, then the supplied fallback.
|
|
29
|
+
*/
|
|
24
30
|
protected setError(err: any, fallback: string): void {
|
|
25
|
-
this.errorMessage.set(err?.error?.message ?? fallback);
|
|
31
|
+
this.errorMessage.set(err?.error?.detail ?? err?.error?.message ?? fallback);
|
|
26
32
|
this.loading.set(false);
|
|
27
33
|
}
|
|
28
34
|
|