@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
|
@@ -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
|
|
13
|
-
<button
|
|
12
|
+
<button matButton="elevated" class="primary" (click)="onSearch()">Search</button>
|
|
13
|
+
<button matButton (click)="onClear()">Clear</button>
|
|
14
14
|
@if (canCreate()) {
|
|
15
|
-
<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
|
|
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,
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* row
|
|
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
|
|
56
|
-
if (!this.apiName && data['apiName']) this.apiName
|
|
57
|
-
if (!this.targetRoute && data['targetRoute']) this.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 (
|
|
64
|
-
(
|
|
53
|
+
if (apiName.endsWith(page.RestUri!) ||
|
|
54
|
+
(tableName && page.RestUri!.includes(tableName))
|
|
65
55
|
) {
|
|
66
56
|
if (page.FilterOnList) {
|
|
67
|
-
this.targetRoute
|
|
57
|
+
this.targetRoute.set('/' + menu.Id!.toLowerCase() + '/' + page.ItemId!);
|
|
68
58
|
} else {
|
|
69
|
-
this.targetRoute
|
|
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
|
-
|
|
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([
|
|
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
|
-
|
|
123
|
-
|
|
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([
|
|
118
|
+
this.router.navigate([targetRoute], {queryParams: {'_action': 'create'}});
|
|
130
119
|
}
|
|
131
120
|
}
|
package/src/index.ts
CHANGED
package/src/model/appdata.ts
CHANGED
|
@@ -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 `<
|
|
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
|
-
// `<
|
|
209
|
+
// `<sail-checkout-button [priceId]>` / `BillingService.createCheckout({ priceId })`.
|
|
210
210
|
priceId?: string;
|
|
211
211
|
}
|
|
212
212
|
|
package/src/model/common.ts
CHANGED
|
@@ -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 =
|
|
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 {
|
|
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
|
|
28
|
-
protected
|
|
29
|
-
protected
|
|
30
|
-
protected
|
|
31
|
-
protected
|
|
32
|
-
protected
|
|
33
|
-
protected
|
|
34
|
-
protected
|
|
35
|
-
protected
|
|
36
|
-
protected
|
|
37
|
-
protected
|
|
38
|
-
protected
|
|
39
|
-
protected
|
|
40
|
-
protected
|
|
41
|
-
protected
|
|
42
|
-
protected
|
|
43
|
-
protected
|
|
44
|
-
protected
|
|
45
|
-
protected
|
|
46
|
-
protected
|
|
47
|
-
protected
|
|
48
|
-
protected
|
|
49
|
-
protected
|
|
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
|
|
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 {
|
|
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 {
|
|
2
|
-
import {
|
|
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 =
|
|
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 {
|
|
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
|
}
|
package/src/util/text.ts
ADDED
|
@@ -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
|
+
}
|