@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.
Files changed (43) hide show
  1. package/README.md +300 -1
  2. package/package.json +2 -2
  3. package/src/component/abstract/base_table.ts +59 -17
  4. package/src/component/abstract/base_view.ts +9 -9
  5. package/src/component/billing/checkout_button.html +1 -1
  6. package/src/component/billing/plan_selector.html +1 -1
  7. package/src/component/form/form_field.html +1 -1
  8. package/src/component/form/form_field.ts +7 -3
  9. package/src/component/form/form_table.html +4 -4
  10. package/src/component/form/form_table.ts +1 -1
  11. package/src/component/login/account_deletion.html +1 -1
  12. package/src/component/login/account_deletion.ts +4 -1
  13. package/src/component/login/chpass_component.html +1 -1
  14. package/src/component/login/confirm_chpass_component.html +1 -1
  15. package/src/component/login/confirm_register_component.html +1 -1
  16. package/src/component/login/login_component.html +1 -1
  17. package/src/component/login/otp_input.html +3 -3
  18. package/src/component/login/register_component.html +1 -1
  19. package/src/component/navigation/navigation.html +3 -3
  20. package/src/component/payment/user_payment_methods.html +46 -0
  21. package/src/component/payment/user_payment_methods.ts +10 -58
  22. package/src/component/payout/bank_info_form.html +59 -0
  23. package/src/component/payout/bank_info_form.ts +21 -77
  24. package/src/component/payout/provider_onboarding.html +70 -0
  25. package/src/component/payout/provider_onboarding.ts +9 -80
  26. package/src/component/security/trusted_devices.html +4 -4
  27. package/src/component/security/twofactor_setup.html +4 -4
  28. package/src/component/security/twofactor_verify.html +1 -1
  29. package/src/component/table/table_detail.html +28 -7
  30. package/src/component/table/table_detail.ts +40 -3
  31. package/src/component/table/table_edit.html +8 -8
  32. package/src/component/table/table_edit.ts +25 -14
  33. package/src/component/table/table_list.html +26 -5
  34. package/src/component/table/table_list.ts +41 -7
  35. package/src/component/table/table_lookup.html +4 -4
  36. package/src/component/table/table_lookup.ts +10 -9
  37. package/src/component/table/table_report.html +1 -1
  38. package/src/component/table/table_search.html +14 -4
  39. package/src/component/table/table_search.ts +55 -17
  40. package/src/model/appdata.ts +20 -6
  41. package/src/service/auth.service.ts +9 -0
  42. package/src/service/rest_service.ts +12 -0
  43. package/src/service/rest_url.ts +4 -3
@@ -1,9 +1,12 @@
1
- import { ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, Input, OnInit, ViewEncapsulation } from "@angular/core";
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
- @Input() override tableName = '';
20
- @Input() apiName: string = '';
21
- @Input() targetRoute: string = '';
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 = data['tableName'];
31
- if (!this.apiName && data['apiName']) this.apiName = data['apiName'];
32
- if (!this.targetRoute && data['targetRoute']) this.targetRoute = data['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 (this.apiName.endsWith(page.RestUri!) ||
39
- (this.tableName && page.RestUri!.includes(this.tableName))
74
+ if (apiName.endsWith(page.RestUri!) ||
75
+ (tableName && page.RestUri!.includes(tableName))
40
76
  ) {
41
77
  if (page.FilterOnList) {
42
- this.targetRoute = '/' + menu.Id!.toLowerCase() + '/' + page.ItemId!;
78
+ this.targetRoute.set('/' + menu.Id!.toLowerCase() + '/' + page.ItemId!);
43
79
  } else {
44
- this.targetRoute = '/' + menu.Id!.toLowerCase() + '/' + page.ItemId! + '/list';
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
- if (!this.targetRoute) {
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([this.targetRoute], {queryParams: this.buildSearchTerms(this.editableRecord)});
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
- if (!this.targetRoute) {
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([this.targetRoute], {queryParams: {'_action': 'create'}});
142
+ this.router.navigate([targetRoute], {queryParams: {'_action': 'create'}});
105
143
  }
106
144
  }
@@ -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: string;
61
- caption: string;
62
- method: string;
63
- enable: string;
64
- authorityCheck: string;
65
- recordSpecific: string;
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
  }
@@ -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; only set-default has a
41
- // custom endpoint (atomic multi-row UPDATE).
42
- paymentMethodSetDefaultURL: '/api/v1/payment-methods/set-default',
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 */