@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.
Files changed (68) hide show
  1. package/README.md +419 -15
  2. package/package.json +2 -2
  3. package/src/component/abstract/base_async.ts +8 -2
  4. package/src/component/abstract/base_table.ts +108 -49
  5. package/src/component/abstract/base_view.ts +32 -45
  6. package/src/component/billing/checkout_button.html +1 -1
  7. package/src/component/billing/checkout_button.ts +1 -1
  8. package/src/component/billing/payment_methods.ts +1 -1
  9. package/src/component/billing/plan_selector.html +1 -1
  10. package/src/component/billing/plan_selector.ts +1 -1
  11. package/src/component/form/form_field.html +1 -1
  12. package/src/component/form/form_field.ts +8 -7
  13. package/src/component/form/form_record.html +2 -2
  14. package/src/component/form/form_record.ts +1 -1
  15. package/src/component/form/form_table.html +4 -4
  16. package/src/component/form/form_table.ts +2 -2
  17. package/src/component/login/account_deletion.html +1 -1
  18. package/src/component/login/account_deletion.ts +5 -2
  19. package/src/component/login/chpass_component.html +1 -1
  20. package/src/component/login/chpass_component.ts +11 -16
  21. package/src/component/login/confirm_chpass_component.html +1 -1
  22. package/src/component/login/confirm_chpass_component.ts +9 -15
  23. package/src/component/login/confirm_register_component.html +1 -1
  24. package/src/component/login/confirm_register_component.ts +9 -15
  25. package/src/component/login/consent_gate.ts +1 -1
  26. package/src/component/login/login_component.html +2 -2
  27. package/src/component/login/login_component.ts +1 -1
  28. package/src/component/login/otp_input.html +3 -3
  29. package/src/component/login/otp_input.ts +1 -1
  30. package/src/component/login/register_component.html +1 -1
  31. package/src/component/login/register_component.ts +1 -1
  32. package/src/component/login/social_login.ts +1 -1
  33. package/src/component/navigation/navigation.html +3 -3
  34. package/src/component/navigation/navigation.ts +10 -11
  35. package/src/component/payment/user_payment_methods.html +46 -0
  36. package/src/component/payment/user_payment_methods.ts +10 -58
  37. package/src/component/payout/bank_info_form.html +59 -0
  38. package/src/component/payout/bank_info_form.ts +21 -77
  39. package/src/component/payout/provider_onboarding.html +70 -0
  40. package/src/component/payout/provider_onboarding.ts +9 -80
  41. package/src/component/security/trusted_devices.html +4 -4
  42. package/src/component/security/trusted_devices.ts +1 -1
  43. package/src/component/security/twofactor_setup.html +4 -4
  44. package/src/component/security/twofactor_setup.ts +18 -26
  45. package/src/component/security/twofactor_verify.html +1 -1
  46. package/src/component/security/twofactor_verify.ts +10 -12
  47. package/src/component/table/table_detail.html +12 -12
  48. package/src/component/table/table_detail.ts +28 -33
  49. package/src/component/table/table_edit.html +20 -10
  50. package/src/component/table/table_edit.ts +29 -18
  51. package/src/component/table/table_list.html +5 -5
  52. package/src/component/table/table_list.ts +24 -48
  53. package/src/component/table/table_lookup.html +4 -4
  54. package/src/component/table/table_lookup.ts +11 -12
  55. package/src/component/table/table_report.html +1 -1
  56. package/src/component/table/table_report.ts +11 -14
  57. package/src/component/table/table_search.html +5 -5
  58. package/src/component/table/table_search.ts +31 -42
  59. package/src/index.ts +2 -0
  60. package/src/model/appdata.ts +2 -2
  61. package/src/model/common.ts +39 -1
  62. package/src/service/auth.service.ts +26 -26
  63. package/src/service/base_rest.service.ts +20 -0
  64. package/src/service/billing.service.ts +3 -9
  65. package/src/service/payout.service.ts +3 -9
  66. package/src/service/rest_service.ts +5 -5
  67. package/src/service/user_payment_method.service.ts +4 -8
  68. package/src/util/text.ts +19 -0
@@ -6,17 +6,17 @@
6
6
  }
7
7
  </h1>
8
8
  <div class="page-container">
9
- <record-form [tableName]="tableName" [record]="editableRecord" [fields]="searchColumns" [isReadOnly]="isReadOnlyBound"></record-form>
9
+ <sail-record-form [tableName]="tableName()" [record]="editableRecord" [fields]="searchColumns" [isReadOnly]="isReadOnlyBound"></sail-record-form>
10
10
  </div>
11
11
  <div class="search-actions">
12
- <button mat-raised-button class="primary" (click)="onSearch()">Search</button>
13
- <button mat-button (click)="onClear()">Clear</button>
12
+ <button matButton="elevated" class="primary" (click)="onSearch()">Search</button>
13
+ <button matButton (click)="onClear()">Clear</button>
14
14
  @if (canCreate()) {
15
- <button mat-raised-button class="accent" (click)="onAddRecord()">New Record</button>
15
+ <button matButton="elevated" class="accent" (click)="onAddRecord()">New Record</button>
16
16
  }
17
17
  @for (action of getActions(false); track action.action) {
18
18
  @if (canExecuteAction(action)) {
19
- <button mat-stroked-button type="button"
19
+ <button matButton="outlined" type="button"
20
20
  [title]="action.caption"
21
21
  (click)="executeAction(action)">
22
22
  @if (action.icon) { <mat-icon>{{ action.icon }}</mat-icon> }
@@ -1,15 +1,13 @@
1
- import { ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, Input, OnInit, ViewEncapsulation } from "@angular/core";
1
+ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, input, linkedSignal, OnInit, ViewEncapsulation } from "@angular/core";
2
2
  import { MatButtonModule } from "@angular/material/button";
3
3
  import { MatIconModule } from "@angular/material/icon";
4
4
  import { ActivatedRoute, Router } from "@angular/router";
5
5
  import { BaseForm } from "../abstract/base_form";
6
6
  import { RecordForm } from "../form/form_record";
7
7
  import { ApplicationMenu } from "../../model/common";
8
- import { TableAction } from "../../model/appdata";
9
- import { BackendService } from "../../service/rest_service";
10
8
 
11
9
  @Component({
12
- selector: 'table-search',
10
+ selector: 'sail-table-search',
13
11
  changeDetection: ChangeDetectionStrategy.OnPush,
14
12
  encapsulation: ViewEncapsulation.None,
15
13
  templateUrl: './table_search.html',
@@ -20,53 +18,45 @@ import { BackendService } from "../../service/rest_service";
20
18
  ],
21
19
  })
22
20
  export class TableSearch extends BaseForm implements OnInit {
23
- @Input() override tableName = '';
24
- @Input() apiName: string = '';
25
- @Input() targetRoute: string = '';
21
+ protected readonly tableNameInput = input('', { alias: 'tableName' });
22
+ protected readonly apiNameInput = input('', { alias: 'apiName' });
23
+ protected readonly targetRouteInput = input('', { alias: 'targetRoute' });
24
+
25
+ override readonly tableName = linkedSignal<string, string>({ source: () => this.tableNameInput(), computation: (v, p) => v || p?.value || '' });
26
+ readonly apiName = linkedSignal<string, string>({ source: () => this.apiNameInput(), computation: (v, p) => v || p?.value || '' });
27
+ readonly targetRoute = linkedSignal<string, string>({ source: () => this.targetRouteInput(), computation: (v, p) => v || p?.value || '' });
28
+
26
29
  searchColumns: string[] = [];
27
30
 
28
31
  private readonly router = inject(Router);
29
32
  private readonly route = inject(ActivatedRoute);
30
33
  private readonly cdr = inject(ChangeDetectorRef);
31
- private readonly backendService = inject(BackendService);
32
34
 
33
35
  /**
34
- * Fire a table-level TableAction from the search-screen toolbar.
35
- * Record-specific actions never show up here — search has no
36
- * row context.
36
+ * Search screen surfaces only table-level actions; record-specific actions
37
+ * never appear here (no row context). `executeAction()` is inherited from
38
+ * BaseTable; no per-screen refresh needed since search has no row list.
37
39
  */
38
- executeAction(action: TableAction): void {
39
- if (!this.canExecuteAction(action)) {
40
- alert(`Missing authorization for action ${action.authorityObject}/${action.authorityCheck}`);
41
- return;
42
- }
43
- if (action.confirmMessage && !confirm(action.confirmMessage)) return;
44
- this.backendService.executeAction(action.method, {}).subscribe({
45
- next: () => {/* no-op — search screen has no list to refresh */},
46
- error: (err) => {
47
- console.error(`Action ${action.action} failed`, err);
48
- alert(err?.error?.detail ?? `Failed to ${action.caption}`);
49
- },
50
- });
51
- }
52
40
 
53
41
  ngOnInit() {
54
42
  const data = this.route.snapshot.data;
55
- if (!this.tableName && data['tableName']) this.tableName = data['tableName'];
56
- if (!this.apiName && data['apiName']) this.apiName = data['apiName'];
57
- if (!this.targetRoute && data['targetRoute']) this.targetRoute = data['targetRoute'];
43
+ if (!this.tableName() && data['tableName']) this.tableName.set(data['tableName']);
44
+ if (!this.apiName() && data['apiName']) this.apiName.set(data['apiName']);
45
+ if (!this.targetRoute() && data['targetRoute']) this.targetRoute.set(data['targetRoute']);
58
46
  this.title = this.getCaption() + ' Search';
59
- if (!this.targetRoute && (this.apiName || this.tableName)) {
47
+ if (!this.targetRoute() && (this.apiName() || this.tableName())) {
60
48
  this.cacheService.getMenus().subscribe((menus: ApplicationMenu[]) => {
49
+ const apiName = this.apiName();
50
+ const tableName = this.tableName();
61
51
  for (const menu of menus) {
62
52
  for (const page of menu.ApplicationMenuItems!) {
63
- if (this.apiName.endsWith(page.RestUri!) ||
64
- (this.tableName && page.RestUri!.includes(this.tableName))
53
+ if (apiName.endsWith(page.RestUri!) ||
54
+ (tableName && page.RestUri!.includes(tableName))
65
55
  ) {
66
56
  if (page.FilterOnList) {
67
- this.targetRoute = '/' + menu.Id!.toLowerCase() + '/' + page.ItemId!;
57
+ this.targetRoute.set('/' + menu.Id!.toLowerCase() + '/' + page.ItemId!);
68
58
  } else {
69
- this.targetRoute = '/' + menu.Id!.toLowerCase() + '/' + page.ItemId! + '/list';
59
+ this.targetRoute.set('/' + menu.Id!.toLowerCase() + '/' + page.ItemId! + '/list');
70
60
  }
71
61
  this.cdr.markForCheck();
72
62
  return;
@@ -80,11 +70,12 @@ export class TableSearch extends BaseForm implements OnInit {
80
70
  }
81
71
 
82
72
  onSearch() {
83
- if (!this.targetRoute) {
73
+ const targetRoute = this.targetRoute();
74
+ if (!targetRoute) {
84
75
  alert('Navigation target not found for this table');
85
76
  return;
86
77
  }
87
- this.router.navigate([this.targetRoute], {queryParams: this.buildSearchTerms(this.editableRecord)});
78
+ this.router.navigate([targetRoute], {queryParams: this.buildSearchTerms(this.editableRecord)});
88
79
  }
89
80
 
90
81
  onClear() {
@@ -106,7 +97,7 @@ export class TableSearch extends BaseForm implements OnInit {
106
97
  */
107
98
  private emptySearchRecord(): any {
108
99
  const rec = this.emptyRecord();
109
- const tableDef = this.cacheService.getTableDefinition(this.tableName);
100
+ const tableDef = this.cacheService.getTableDefinition(this.tableName());
110
101
  if (tableDef?.Columns) {
111
102
  for (const col of tableDef.Columns) {
112
103
  if (col.DataType === 'boolean') {
@@ -118,14 +109,12 @@ export class TableSearch extends BaseForm implements OnInit {
118
109
  }
119
110
 
120
111
  onAddRecord() {
121
- if (!this.canCreate()) {
122
- alert('Missing authorization to create records');
123
- return;
124
- }
125
- if (!this.targetRoute) {
112
+ if (!this.requireAuth(() => this.canCreate(), 'create')) return;
113
+ const targetRoute = this.targetRoute();
114
+ if (!targetRoute) {
126
115
  alert('Navigation target not found for this table');
127
116
  return;
128
117
  }
129
- this.router.navigate([this.targetRoute], {queryParams: {'_action': 'create'}});
118
+ this.router.navigate([targetRoute], {queryParams: {'_action': 'create'}});
130
119
  }
131
120
  }
package/src/index.ts CHANGED
@@ -45,3 +45,5 @@ export * from './service/api-response.interceptor';
45
45
  export * from './service/rest_service';
46
46
  export * from './service/rest_url';
47
47
  export * from './service/label.service';
48
+ export * from './service/base_rest.service';
49
+ export * from './util/text';
@@ -197,7 +197,7 @@ export interface PartnerRegistration {
197
197
  }
198
198
 
199
199
  // PublicPlan — shape returned by GET /public/plans. Used on the registration
200
- // page to render the plan picker and to drive `<app-checkout-button>` flows.
200
+ // page to render the plan picker and to drive `<sail-checkout-button>` flows.
201
201
  export interface PublicPlan {
202
202
  id: string;
203
203
  caption: string;
@@ -206,7 +206,7 @@ export interface PublicPlan {
206
206
  // Stripe price ID (or provider-equivalent). Optional because keel only
207
207
  // populates it when a plan is wired to a billing provider; FREE plans and
208
208
  // not-yet-priced tiers leave it empty. Pass straight to
209
- // `<app-checkout-button [priceId]>` / `BillingService.createCheckout({ priceId })`.
209
+ // `<sail-checkout-button [priceId]>` / `BillingService.createCheckout({ priceId })`.
210
210
  priceId?: string;
211
211
  }
212
212
 
@@ -1,7 +1,45 @@
1
1
  import { IsString, IsNumeric } from './decorator';
2
2
 
3
+ /**
4
+ * SIUD operation codes that travel on every row through keel's generic CRUD.
5
+ * Sail tags records with these so the backend knows what to do with each row
6
+ * inside a bulk POST. Mirror of keel's `data.SiudOpCode` constants.
7
+ *
8
+ * S = Select-only (default for fetched rows — no change)
9
+ * I = Insert (locally created, not yet persisted)
10
+ * U = Update (existing row, scalar fields changed)
11
+ * D = Delete (existing row, mark for removal)
12
+ * R = Read-only (existing row, no change after dirty-check)
13
+ *
14
+ * Always reference via `OpCode.Insert` etc. — never the bare string. The
15
+ * `OpCodeValue` union type lets TS catch typos at compile time.
16
+ */
17
+ export const OpCode = {
18
+ Select: 'S',
19
+ Insert: 'I',
20
+ Update: 'U',
21
+ Delete: 'D',
22
+ Readonly: 'R',
23
+ } as const;
24
+
25
+ export type OpCodeValue = typeof OpCode[keyof typeof OpCode];
26
+
27
+ /**
28
+ * Lookup-style values on `TableColumn.LookupStyle` — controls how the lookup
29
+ * renders in the form. Mirror of keel's basis `column.lookup_style` enum.
30
+ *
31
+ * D = Dropdown (constant_value rows preloaded)
32
+ * S = Search (TableLookup dialog with full search UI)
33
+ */
34
+ export const LookupStyle = {
35
+ Dropdown: 'D',
36
+ Search: 'S',
37
+ } as const;
38
+
39
+ export type LookupStyleValue = typeof LookupStyle[keyof typeof LookupStyle];
40
+
3
41
  export class SiudAction {
4
- op_code = 'S';
42
+ op_code: OpCodeValue = OpCode.Select;
5
43
  }
6
44
 
7
45
  export class UserAccountPolicy extends SiudAction {
@@ -1,5 +1,5 @@
1
1
  import { Injectable, WritableSignal, inject, signal } from '@angular/core';
2
- import { HttpClient, HttpParams } from '@angular/common/http';
2
+ import { HttpParams } from '@angular/common/http';
3
3
  import { map, take, tap } from 'rxjs/operators';
4
4
  import { ApplicationData, AuthSummary, ConfirmRegisterResponse, DictionaryPath, LoginResponse2FA, PartnerRegistration, RestReport, TableDefinition, TrustedDevice, TwoFactorSetupResponse, TwoFactorVerifyRequest, TwoFactorVerifyResponse } from '../model/appdata';
5
5
  import {
@@ -14,39 +14,39 @@ import { Observable, ReplaySubject } from 'rxjs';
14
14
  import { ApplicationMenu, ConstantValue } from '../model/common';
15
15
  import { CanActivateFn, Router, Routes } from '@angular/router';
16
16
  import { SAIL_GUI_CONFIG, SailGuiConfig, DEFAULT_CONFIG } from '../config';
17
+ import { BaseRestService } from './base_rest.service';
17
18
 
18
19
  @Injectable()
19
- export abstract class BaseAuthService {
20
- private readonly http = inject(HttpClient);
20
+ export abstract class BaseAuthService extends BaseRestService {
21
21
  protected readonly router = inject(Router);
22
22
  protected readonly guiConfig: SailGuiConfig = inject(SAIL_GUI_CONFIG, {optional: true}) ?? DEFAULT_CONFIG;
23
23
  protected cache!: ApplicationData;
24
24
  private readonly appData$ = new ReplaySubject<ApplicationData>(1);
25
25
  private readonly authIndex = new Map<string, AuthSummary[]>();
26
26
 
27
- protected get appdataUrl() { return RestURL.httpHost + RestURL.appdataURL; }
28
- protected get loginUrl() { return RestURL.httpHost + RestURL.loginURL; }
29
- protected get registerUrl() { return RestURL.httpHost + RestURL.registerURL; }
30
- protected get chpassUrl() { return RestURL.httpHost + RestURL.chpassURL; }
31
- protected get confirmRegisterUrl() { return RestURL.httpHost + RestURL.confirmRegisterURL; }
32
- protected get confirmChpassUrl() { return RestURL.httpHost + RestURL.confirmChpassURL; }
33
- protected get loginGoogleUrl() { return RestURL.httpHost + RestURL.loginGoogleURL; }
34
- protected get twoFactorSetupUrl() { return RestURL.httpHost + RestURL.twoFactorSetupURL; }
35
- protected get twoFactorVerifyUrl() { return RestURL.httpHost + RestURL.twoFactorVerifyURL; }
36
- protected get twoFactorDisableUrl() { return RestURL.httpHost + RestURL.twoFactorDisableURL; }
37
- protected get twoFactorLoginVerifyUrl() { return RestURL.httpHost + RestURL.twoFactorLoginVerifyURL; }
38
- protected get twoFactorBackupVerifyUrl() { return RestURL.httpHost + RestURL.twoFactorBackupVerifyURL; }
39
- protected get trustedDeviceListUrl() { return RestURL.httpHost + RestURL.trustedDeviceListURL; }
40
- protected get trustedDeviceRegisterUrl() { return RestURL.httpHost + RestURL.trustedDeviceRegisterURL; }
41
- protected get trustedDeviceRevokeUrl() { return RestURL.httpHost + RestURL.trustedDeviceRevokeURL; }
42
- protected get otpSendUrl() { return RestURL.httpHost + RestURL.otpSendURL; }
43
- protected get otpVerifyUrl() { return RestURL.httpHost + RestURL.otpVerifyURL; }
44
- protected get otpResendUrl() { return RestURL.httpHost + RestURL.otpResendURL; }
45
- protected get loginSocialUrl() { return RestURL.httpHost + RestURL.loginSocialURL; }
46
- protected get logoutEverywhereUrl() { return RestURL.httpHost + RestURL.logoutEverywhereURL; }
47
- protected get deleteAccountUrl() { return RestURL.httpHost + RestURL.deleteAccountURL; }
48
- protected get pushRegisterUrl() { return RestURL.httpHost + RestURL.pushRegisterURL; }
49
- protected get pushRevokeUrl() { return RestURL.httpHost + RestURL.pushRevokeURL; }
27
+ protected readonly appdataUrl = this.url(RestURL.appdataURL);
28
+ protected readonly loginUrl = this.url(RestURL.loginURL);
29
+ protected readonly registerUrl = this.url(RestURL.registerURL);
30
+ protected readonly chpassUrl = this.url(RestURL.chpassURL);
31
+ protected readonly confirmRegisterUrl = this.url(RestURL.confirmRegisterURL);
32
+ protected readonly confirmChpassUrl = this.url(RestURL.confirmChpassURL);
33
+ protected readonly loginGoogleUrl = this.url(RestURL.loginGoogleURL);
34
+ protected readonly twoFactorSetupUrl = this.url(RestURL.twoFactorSetupURL);
35
+ protected readonly twoFactorVerifyUrl = this.url(RestURL.twoFactorVerifyURL);
36
+ protected readonly twoFactorDisableUrl = this.url(RestURL.twoFactorDisableURL);
37
+ protected readonly twoFactorLoginVerifyUrl = this.url(RestURL.twoFactorLoginVerifyURL);
38
+ protected readonly twoFactorBackupVerifyUrl = this.url(RestURL.twoFactorBackupVerifyURL);
39
+ protected readonly trustedDeviceListUrl = this.url(RestURL.trustedDeviceListURL);
40
+ protected readonly trustedDeviceRegisterUrl = this.url(RestURL.trustedDeviceRegisterURL);
41
+ protected readonly trustedDeviceRevokeUrl = this.url(RestURL.trustedDeviceRevokeURL);
42
+ protected readonly otpSendUrl = this.url(RestURL.otpSendURL);
43
+ protected readonly otpVerifyUrl = this.url(RestURL.otpVerifyURL);
44
+ protected readonly otpResendUrl = this.url(RestURL.otpResendURL);
45
+ protected readonly loginSocialUrl = this.url(RestURL.loginSocialURL);
46
+ protected readonly logoutEverywhereUrl = this.url(RestURL.logoutEverywhereURL);
47
+ protected readonly deleteAccountUrl = this.url(RestURL.deleteAccountURL);
48
+ protected readonly pushRegisterUrl = this.url(RestURL.pushRegisterURL);
49
+ protected readonly pushRevokeUrl = this.url(RestURL.pushRevokeURL);
50
50
 
51
51
  token: string | null = null;
52
52
  readonly isLoggedIn = signal(false);
@@ -0,0 +1,20 @@
1
+ import { HttpClient } from '@angular/common/http';
2
+ import { inject } from '@angular/core';
3
+ import { RestURL } from './rest_url';
4
+
5
+ /**
6
+ * Foundation for sail's REST API clients. Owns the shared `http` injection
7
+ * and the `host + path` URL builder so subclasses don't repeat them.
8
+ *
9
+ * Subclasses set `providedIn: 'root'` on their own `@Injectable` decorator
10
+ * (this class itself stays unannotated — Angular resolves it through the
11
+ * subclass).
12
+ */
13
+ export abstract class BaseRestService {
14
+ protected readonly http = inject(HttpClient);
15
+
16
+ /** Prefix any `RestURL.*URL` path with the configured backend host. */
17
+ protected url(path: string): string {
18
+ return RestURL.httpHost + path;
19
+ }
20
+ }
@@ -1,7 +1,7 @@
1
- import { Injectable, inject } from '@angular/core';
2
- import { HttpClient } from '@angular/common/http';
1
+ import { Injectable } from '@angular/core';
3
2
  import { Observable } from 'rxjs';
4
3
  import { RestURL } from './rest_url';
4
+ import { BaseRestService } from './base_rest.service';
5
5
  import {
6
6
  CheckoutRequest,
7
7
  CheckoutResponse,
@@ -19,13 +19,7 @@ import {
19
19
  * keel/SHARED_PAYMENT.md for the backend contract.
20
20
  */
21
21
  @Injectable({ providedIn: 'root' })
22
- export class BillingService {
23
- private readonly http = inject(HttpClient);
24
-
25
- private url(path: string): string {
26
- return RestURL.httpHost + path;
27
- }
28
-
22
+ export class BillingService extends BaseRestService {
29
23
  /** Public catalog — safe to call before login. */
30
24
  listPlans(): Observable<PublicPlan[]> {
31
25
  return this.http.get<PublicPlan[]>(this.url(RestURL.plansURL));
@@ -1,7 +1,7 @@
1
- import { inject, Injectable } from '@angular/core';
2
- import { HttpClient } from '@angular/common/http';
1
+ import { Injectable } from '@angular/core';
3
2
  import { Observable } from 'rxjs';
4
3
  import { RestURL } from './rest_url';
4
+ import { BaseRestService } from './base_rest.service';
5
5
  import {
6
6
  PayoutOnboardingSession,
7
7
  ReusableAccount,
@@ -19,13 +19,7 @@ import {
19
19
  * RestURL.payout*URL.
20
20
  */
21
21
  @Injectable({ providedIn: 'root' })
22
- export class PayoutService {
23
- private readonly http = inject(HttpClient);
24
-
25
- private url(path: string): string {
26
- return RestURL.httpHost + path;
27
- }
28
-
22
+ export class PayoutService extends BaseRestService {
29
23
  /** Launch the hosted-KYC flow. Returns a URL to open + provider-side handle. */
30
24
  startOnboarding(): Observable<PayoutOnboardingSession> {
31
25
  return this.http.post<PayoutOnboardingSession>(this.url(RestURL.payoutOnboardStartURL), {});
@@ -1,13 +1,13 @@
1
- import { HttpClient, HttpErrorResponse, HttpParams } from "@angular/common/http";
2
- import { inject, Injectable } from "@angular/core";
1
+ import { HttpErrorResponse, HttpParams } from "@angular/common/http";
2
+ import { Injectable } from "@angular/core";
3
3
  import { catchError, map, Observable, throwError } from "rxjs";
4
4
  import { PaginatedList } from "../model/appdata";
5
5
  import { RestURL } from "./rest_url";
6
+ import { BaseRestService } from "./base_rest.service";
6
7
 
7
8
  @Injectable({providedIn: 'root'})
8
- export class BackendService {
9
- protected readonly prefix = RestURL.httpHost + RestURL.api_prefix;
10
- protected readonly http = inject(HttpClient);
9
+ export class BackendService extends BaseRestService {
10
+ protected readonly prefix = this.url(RestURL.api_prefix);
11
11
 
12
12
  protected handleError(error: HttpErrorResponse) {
13
13
  if (error.status === 0) {
@@ -1,8 +1,8 @@
1
- import { inject, Injectable } from '@angular/core';
2
- import { HttpClient } from '@angular/common/http';
1
+ import { Injectable, inject } from '@angular/core';
3
2
  import { Observable } from 'rxjs';
4
3
  import { RestURL } from './rest_url';
5
4
  import { BackendService } from './rest_service';
5
+ import { BaseRestService } from './base_rest.service';
6
6
  import { UserPaymentMethod } from '../model/appdata';
7
7
 
8
8
  /**
@@ -17,8 +17,7 @@ import { UserPaymentMethod } from '../model/appdata';
17
17
  * single-row CRUD can't express.
18
18
  */
19
19
  @Injectable({ providedIn: 'root' })
20
- export class UserPaymentMethodService {
21
- private readonly http = inject(HttpClient);
20
+ export class UserPaymentMethodService extends BaseRestService {
22
21
  private readonly backend = inject(BackendService);
23
22
 
24
23
  /**
@@ -40,9 +39,6 @@ export class UserPaymentMethodService {
40
39
  * Custom endpoint — see service-class doc for the rationale.
41
40
  */
42
41
  setDefault(id: number): Observable<{ message: string }> {
43
- return this.http.post<{ message: string }>(
44
- RestURL.httpHost + RestURL.paymentMethodSetDefaultURL,
45
- { id },
46
- );
42
+ return this.http.post<{ message: string }>(this.url(RestURL.paymentMethodSetDefaultURL), { id });
47
43
  }
48
44
  }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Convert a snake_case / lower_case identifier to Title Case.
3
+ *
4
+ * titleCase('user_payment_method') -> 'User Payment Method'
5
+ * titleCase('UserId') -> 'Userid' (no camelCase split)
6
+ *
7
+ * Used as the fallback label for tables and columns when no explicit
8
+ * caption is set in `basis.table_header.Caption` / `basis.column.Caption`.
9
+ * Mirrors keel's `common.PascalCase`-style helpers on the frontend side
10
+ * so caption rendering is consistent across consumers.
11
+ */
12
+ export function titleCase(str: string): string {
13
+ return str
14
+ .replaceAll('_', ' ')
15
+ .toLowerCase()
16
+ .split(' ')
17
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
18
+ .join(' ');
19
+ }