@nauticana/sail 0.6.1 → 0.8.0
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 +300 -1
- package/package.json +2 -2
- package/src/component/abstract/base_table.ts +59 -17
- package/src/component/abstract/base_view.ts +9 -9
- package/src/component/billing/checkout_button.html +1 -1
- package/src/component/billing/plan_selector.html +1 -1
- package/src/component/form/form_field.html +1 -1
- package/src/component/form/form_field.ts +7 -3
- package/src/component/form/form_table.html +4 -4
- package/src/component/form/form_table.ts +1 -1
- package/src/component/login/account_deletion.html +1 -1
- package/src/component/login/account_deletion.ts +4 -1
- package/src/component/login/chpass_component.html +1 -1
- package/src/component/login/confirm_chpass_component.html +1 -1
- package/src/component/login/confirm_register_component.html +1 -1
- package/src/component/login/login_component.html +1 -1
- package/src/component/login/otp_input.html +3 -3
- package/src/component/login/register_component.html +1 -1
- package/src/component/navigation/navigation.html +3 -3
- 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/twofactor_setup.html +4 -4
- package/src/component/security/twofactor_verify.html +1 -1
- package/src/component/table/table_detail.html +28 -7
- package/src/component/table/table_detail.ts +40 -3
- package/src/component/table/table_edit.html +8 -8
- package/src/component/table/table_edit.ts +25 -14
- package/src/component/table/table_list.html +26 -5
- package/src/component/table/table_list.ts +41 -7
- package/src/component/table/table_lookup.html +4 -4
- package/src/component/table/table_lookup.ts +10 -9
- package/src/component/table/table_report.html +1 -1
- package/src/component/table/table_search.html +14 -4
- package/src/component/table/table_search.ts +55 -17
- package/src/model/appdata.ts +20 -6
- package/src/service/auth.service.ts +9 -0
- package/src/service/rest_service.ts +12 -0
- package/src/service/rest_url.ts +4 -3
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, inject,
|
|
1
|
+
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, effect, inject, input, OnInit, signal, ViewEncapsulation } from "@angular/core";
|
|
2
2
|
import { MatButtonModule } from "@angular/material/button";
|
|
3
|
+
import { MatIconModule } from "@angular/material/icon";
|
|
3
4
|
import { ActivatedRoute, Router } from "@angular/router";
|
|
4
5
|
import { BaseForm } from "../abstract/base_form";
|
|
5
6
|
import { RecordForm } from "../form/form_record";
|
|
6
7
|
import { ApplicationMenu } from "../../model/common";
|
|
8
|
+
import { TableAction } from "../../model/appdata";
|
|
9
|
+
import { BackendService } from "../../service/rest_service";
|
|
7
10
|
|
|
8
11
|
@Component({
|
|
9
12
|
selector: 'table-search',
|
|
@@ -12,36 +15,69 @@ import { ApplicationMenu } from "../../model/common";
|
|
|
12
15
|
templateUrl: './table_search.html',
|
|
13
16
|
imports: [
|
|
14
17
|
MatButtonModule,
|
|
18
|
+
MatIconModule,
|
|
15
19
|
RecordForm,
|
|
16
20
|
],
|
|
17
21
|
})
|
|
18
22
|
export class TableSearch extends BaseForm implements OnInit {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
readonly tableNameInput = input('', { alias: 'tableName' });
|
|
24
|
+
readonly apiNameInput = input('', { alias: 'apiName' });
|
|
25
|
+
readonly targetRouteInput = input('', { alias: 'targetRoute' });
|
|
26
|
+
readonly apiName = signal('');
|
|
27
|
+
readonly targetRoute = signal('');
|
|
22
28
|
searchColumns: string[] = [];
|
|
23
29
|
|
|
30
|
+
constructor() {
|
|
31
|
+
super();
|
|
32
|
+
effect(() => { const v = this.tableNameInput(); if (v) this.tableName.set(v); });
|
|
33
|
+
effect(() => { const v = this.apiNameInput(); if (v) this.apiName.set(v); });
|
|
34
|
+
effect(() => { const v = this.targetRouteInput();if (v) this.targetRoute.set(v); });
|
|
35
|
+
}
|
|
36
|
+
|
|
24
37
|
private readonly router = inject(Router);
|
|
25
38
|
private readonly route = inject(ActivatedRoute);
|
|
26
39
|
private readonly cdr = inject(ChangeDetectorRef);
|
|
40
|
+
private readonly backendService = inject(BackendService);
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Fire a table-level TableAction from the search-screen toolbar.
|
|
44
|
+
* Record-specific actions never show up here — search has no
|
|
45
|
+
* row context.
|
|
46
|
+
*/
|
|
47
|
+
executeAction(action: TableAction): void {
|
|
48
|
+
if (!this.canExecuteAction(action)) {
|
|
49
|
+
alert(`Missing authorization for action ${action.authorityObject}/${action.authorityCheck}`);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (action.confirmMessage && !confirm(action.confirmMessage)) return;
|
|
53
|
+
this.backendService.executeAction(action.method, {}).subscribe({
|
|
54
|
+
next: () => {/* no-op — search screen has no list to refresh */},
|
|
55
|
+
error: (err) => {
|
|
56
|
+
console.error(`Action ${action.action} failed`, err);
|
|
57
|
+
alert(err?.error?.detail ?? `Failed to ${action.caption}`);
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
}
|
|
27
61
|
|
|
28
62
|
ngOnInit() {
|
|
29
63
|
const data = this.route.snapshot.data;
|
|
30
|
-
if (!this.tableName && data['tableName']) this.tableName
|
|
31
|
-
if (!this.apiName && data['apiName']) this.apiName
|
|
32
|
-
if (!this.targetRoute && data['targetRoute']) this.targetRoute
|
|
64
|
+
if (!this.tableName() && data['tableName']) this.tableName.set(data['tableName']);
|
|
65
|
+
if (!this.apiName() && data['apiName']) this.apiName.set(data['apiName']);
|
|
66
|
+
if (!this.targetRoute() && data['targetRoute']) this.targetRoute.set(data['targetRoute']);
|
|
33
67
|
this.title = this.getCaption() + ' Search';
|
|
34
|
-
if (!this.targetRoute && (this.apiName || this.tableName)) {
|
|
68
|
+
if (!this.targetRoute() && (this.apiName() || this.tableName())) {
|
|
35
69
|
this.cacheService.getMenus().subscribe((menus: ApplicationMenu[]) => {
|
|
70
|
+
const apiName = this.apiName();
|
|
71
|
+
const tableName = this.tableName();
|
|
36
72
|
for (const menu of menus) {
|
|
37
73
|
for (const page of menu.ApplicationMenuItems!) {
|
|
38
|
-
if (
|
|
39
|
-
(
|
|
74
|
+
if (apiName.endsWith(page.RestUri!) ||
|
|
75
|
+
(tableName && page.RestUri!.includes(tableName))
|
|
40
76
|
) {
|
|
41
77
|
if (page.FilterOnList) {
|
|
42
|
-
this.targetRoute
|
|
78
|
+
this.targetRoute.set('/' + menu.Id!.toLowerCase() + '/' + page.ItemId!);
|
|
43
79
|
} else {
|
|
44
|
-
this.targetRoute
|
|
80
|
+
this.targetRoute.set('/' + menu.Id!.toLowerCase() + '/' + page.ItemId! + '/list');
|
|
45
81
|
}
|
|
46
82
|
this.cdr.markForCheck();
|
|
47
83
|
return;
|
|
@@ -55,11 +91,12 @@ export class TableSearch extends BaseForm implements OnInit {
|
|
|
55
91
|
}
|
|
56
92
|
|
|
57
93
|
onSearch() {
|
|
58
|
-
|
|
94
|
+
const targetRoute = this.targetRoute();
|
|
95
|
+
if (!targetRoute) {
|
|
59
96
|
alert('Navigation target not found for this table');
|
|
60
97
|
return;
|
|
61
98
|
}
|
|
62
|
-
this.router.navigate([
|
|
99
|
+
this.router.navigate([targetRoute], {queryParams: this.buildSearchTerms(this.editableRecord)});
|
|
63
100
|
}
|
|
64
101
|
|
|
65
102
|
onClear() {
|
|
@@ -81,7 +118,7 @@ export class TableSearch extends BaseForm implements OnInit {
|
|
|
81
118
|
*/
|
|
82
119
|
private emptySearchRecord(): any {
|
|
83
120
|
const rec = this.emptyRecord();
|
|
84
|
-
const tableDef = this.cacheService.getTableDefinition(this.tableName);
|
|
121
|
+
const tableDef = this.cacheService.getTableDefinition(this.tableName());
|
|
85
122
|
if (tableDef?.Columns) {
|
|
86
123
|
for (const col of tableDef.Columns) {
|
|
87
124
|
if (col.DataType === 'boolean') {
|
|
@@ -97,10 +134,11 @@ export class TableSearch extends BaseForm implements OnInit {
|
|
|
97
134
|
alert('Missing authorization to create records');
|
|
98
135
|
return;
|
|
99
136
|
}
|
|
100
|
-
|
|
137
|
+
const targetRoute = this.targetRoute();
|
|
138
|
+
if (!targetRoute) {
|
|
101
139
|
alert('Navigation target not found for this table');
|
|
102
140
|
return;
|
|
103
141
|
}
|
|
104
|
-
this.router.navigate([
|
|
142
|
+
this.router.navigate([targetRoute], {queryParams: {'_action': 'create'}});
|
|
105
143
|
}
|
|
106
144
|
}
|
package/src/model/appdata.ts
CHANGED
|
@@ -56,13 +56,27 @@ export interface TableColumn {
|
|
|
56
56
|
Validators?: AbstractControlOptions['validators'];
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
// One custom button registered against a table via the basis
|
|
60
|
+
// table_action seed. Auth gating uses the existing
|
|
61
|
+
// authorization_role_permission path against (authorityObject,
|
|
62
|
+
// authorityCheck, low_limit=tableName) — see canExecute() in
|
|
63
|
+
// BaseAuthService.
|
|
64
|
+
//
|
|
65
|
+
// recordSpecific=true → button renders next to per-row edit/delete.
|
|
66
|
+
// recordSpecific=false → button renders next to the "New Record" toolbar.
|
|
67
|
+
//
|
|
68
|
+
// `method` is the URL POST target the click should hit (resolved by
|
|
69
|
+
// keel from table_name + action_name + optional method_name override).
|
|
59
70
|
export interface TableAction {
|
|
60
|
-
action:
|
|
61
|
-
caption:
|
|
62
|
-
method:
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
71
|
+
action: string; // lowercase action_name
|
|
72
|
+
caption: string;
|
|
73
|
+
method: string; // resolved URL path (POST target)
|
|
74
|
+
icon?: string; // Material icon name; falsy → label-only
|
|
75
|
+
recordSpecific: boolean;
|
|
76
|
+
displayOrder?: number;
|
|
77
|
+
confirmMessage?: string;
|
|
78
|
+
authorityObject: string; // uppercased table_name — for canExecute()
|
|
79
|
+
authorityCheck: string; // uppercased action_name — for canExecute()
|
|
66
80
|
}
|
|
67
81
|
|
|
68
82
|
export interface ForeignKey {
|
|
@@ -471,6 +471,15 @@ export abstract class BaseAuthService {
|
|
|
471
471
|
canCreate(tableName: string) { return this.checkPermission('TABLE', 'INSERT', tableName); }
|
|
472
472
|
canUpdate(tableName: string) { return this.checkPermission('TABLE', 'UPDATE', tableName); }
|
|
473
473
|
canDelete(tableName: string) { return this.checkPermission('TABLE', 'DELETE', tableName); }
|
|
474
|
+
/**
|
|
475
|
+
* Authorization check for a custom TableAction. authorityObject is the
|
|
476
|
+
* uppercased table_name; activity is the uppercased action_name. The
|
|
477
|
+
* grant's low_limit is checked against the table_name (lowercase) —
|
|
478
|
+
* matching the convention populated by keel's loadTableActions.
|
|
479
|
+
*/
|
|
480
|
+
canExecute(authorityObject: string, activity: string, tableName: string): boolean {
|
|
481
|
+
return this.checkPermission(authorityObject, activity, tableName);
|
|
482
|
+
}
|
|
474
483
|
canAccess(pageName: string) { return this.checkPermission('PAGE', 'ACCESS', pageName); }
|
|
475
484
|
canReport(reportName: string) { return this.checkPermission('REPORT', 'ACCESS', reportName); }
|
|
476
485
|
|
|
@@ -91,4 +91,16 @@ export class BackendService {
|
|
|
91
91
|
delete(apiName: string, filter?: {[key: string]: string}): Observable<{message: string}> {
|
|
92
92
|
return this.http.delete<{message: string}>(this.apiUrl(apiName + '/delete'), {params: this.toParams(filter)}).pipe(catchError(this.handleError));
|
|
93
93
|
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Fire a TableAction button. `actionPath` is the resolved URL path
|
|
97
|
+
* the keel REST engine pre-computed onto TableAction.method
|
|
98
|
+
* (e.g. "/user_payment_method/set_default"); pass it verbatim and
|
|
99
|
+
* the call will POST to {host}{api_prefix}{actionPath} with the
|
|
100
|
+
* supplied body. For record-specific actions the body carries the
|
|
101
|
+
* primary key values; for table-level actions it is typically `{}`.
|
|
102
|
+
*/
|
|
103
|
+
executeAction<T = unknown>(actionPath: string, body: Record<string, unknown> = {}): Observable<T> {
|
|
104
|
+
return this.http.post<T>(this.apiUrl(actionPath), body).pipe(catchError(this.handleError));
|
|
105
|
+
}
|
|
94
106
|
}
|
package/src/service/rest_url.ts
CHANGED
|
@@ -37,9 +37,10 @@ export const RestURL = {
|
|
|
37
37
|
payoutReusableLinkURL: '/api/v1/payout/reusable/link',
|
|
38
38
|
payoutStatusURL: '/api/v1/payout/status',
|
|
39
39
|
// User payment methods — list/delete go through keel's generic REST
|
|
40
|
-
// CRUD against the UserSpecific basis table;
|
|
41
|
-
//
|
|
42
|
-
|
|
40
|
+
// CRUD against the UserSpecific basis table; set-default is a
|
|
41
|
+
// TableAction (basis table_action row), mounted at the conventional
|
|
42
|
+
// URL POST /api/v1/user_payment_method/set_default.
|
|
43
|
+
paymentMethodSetDefaultURL: '/api/v1/user_payment_method/set_default',
|
|
43
44
|
};
|
|
44
45
|
|
|
45
46
|
/** Call this at app startup to set the backend host URL */
|