@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
@@ -10,7 +10,7 @@
10
10
  <mat-label>Confirm your password</mat-label>
11
11
  <input matInput type="password" formControlName="password" autocomplete="current-password" required>
12
12
  </mat-form-field>
13
- <button mat-raised-button class="primary" type="submit" [disabled]="!initForm.valid">Enable 2FA</button>
13
+ <button matButton="elevated" class="primary" type="submit" [disabled]="!initForm.valid">Enable 2FA</button>
14
14
  </form>
15
15
  }
16
16
  @case ('qr') {
@@ -32,7 +32,7 @@
32
32
  </div>
33
33
  }
34
34
 
35
- <button mat-raised-button class="primary" (click)="proceedToVerify()">I've saved my backup codes</button>
35
+ <button matButton="elevated" class="primary" (click)="proceedToVerify()">I've saved my backup codes</button>
36
36
  }
37
37
  @case ('verify') {
38
38
  <p class="auth-instructions">Enter the 6-digit code from your authenticator app to confirm setup.</p>
@@ -42,7 +42,7 @@
42
42
  <input matInput type="text" formControlName="code" maxlength="6" autocomplete="one-time-code" required>
43
43
  </mat-form-field>
44
44
 
45
- <button mat-raised-button class="primary" type="submit" [disabled]="!verifyForm.valid">
45
+ <button matButton="elevated" class="primary" type="submit" [disabled]="!verifyForm.valid">
46
46
  Verify and Enable
47
47
  </button>
48
48
  </form>
@@ -59,7 +59,7 @@
59
59
  <mat-label>Current 6-digit code</mat-label>
60
60
  <input matInput type="text" formControlName="code" maxlength="6" autocomplete="one-time-code" required>
61
61
  </mat-form-field>
62
- <button mat-button class="warn" type="submit" [disabled]="!disableForm.valid">Disable 2FA</button>
62
+ <button matButton class="warn" type="submit" [disabled]="!disableForm.valid">Disable 2FA</button>
63
63
  </form>
64
64
  }
65
65
  }
@@ -31,7 +31,7 @@
31
31
  <div class="auth-error" role="alert">{{ errorMessage() }}</div>
32
32
  }
33
33
 
34
- <button mat-raised-button class="primary" type="submit" [disabled]="!verifyForm.valid">
34
+ <button matButton="elevated" class="primary" type="submit" [disabled]="!verifyForm.valid">
35
35
  Verify
36
36
  </button>
37
37
  </form>
@@ -2,7 +2,17 @@
2
2
  <div class="child-table-header">
3
3
  <h3>{{ getCaption() }}</h3>
4
4
  @if (canCreate()) {
5
- <button mat-raised-button class="accent" (click)="addRecord()">New Record</button>
5
+ <button matButton="elevated" class="accent" (click)="addRecord()">New Record</button>
6
+ }
7
+ @for (action of getActions(false); track action.action) {
8
+ @if (canExecuteAction(action)) {
9
+ <button matButton="outlined" type="button"
10
+ [title]="action.caption"
11
+ (click)="executeAction(action)">
12
+ @if (action.icon) { <mat-icon>{{ action.icon }}</mat-icon> }
13
+ {{ action.caption }}
14
+ </button>
15
+ }
6
16
  }
7
17
  </div>
8
18
  <table class="mat-table">
@@ -23,13 +33,13 @@
23
33
  @if (isReadOnly(col)) {
24
34
  {{ record[col] }}
25
35
  } @else {
26
- <dynamic-field [tableName]="tableName" [value]="record[col]" [field]="col" (valueChange)="record[col]=$event" subscriptSizing="dynamic"></dynamic-field>
36
+ <dynamic-field [tableName]="tableName()" [value]="record[col]" [field]="col" (valueChange)="record[col]=$event" subscriptSizing="dynamic"></dynamic-field>
27
37
  }
28
38
  </td>
29
39
  }
30
40
  <td>
31
- <button mat-icon-button (click)="saveRow()" title="Save"><mat-icon>save</mat-icon></button>
32
- <button mat-icon-button (click)="cancelRow(record)" title="Cancel"><mat-icon>cancel</mat-icon></button>
41
+ <button matIconButton (click)="saveRow()" title="Save"><mat-icon>save</mat-icon></button>
42
+ <button matIconButton (click)="cancelRow(record)" title="Cancel"><mat-icon>cancel</mat-icon></button>
33
43
  </td>
34
44
  } @else {
35
45
  @for (col of displayedColumns; track col) {
@@ -37,12 +47,23 @@
37
47
  }
38
48
  <td>
39
49
  @if (canUpdate()) {
40
- <button mat-icon-button (click)="editRow(record)" title="Edit"><mat-icon>edit</mat-icon></button>
50
+ <button matIconButton (click)="editRow(record)" title="Edit"><mat-icon>edit</mat-icon></button>
41
51
  }
42
52
  @if (record[config.opField] === 'D') {
43
- <button mat-icon-button (click)="unDeleteRow(record)" title="Undelete"><mat-icon>restore_from_trash</mat-icon></button>
53
+ <button matIconButton (click)="unDeleteRow(record)" title="Undelete"><mat-icon>restore_from_trash</mat-icon></button>
44
54
  } @else if (canDelete()) {
45
- <button mat-icon-button (click)="deleteRow(record)" title="Delete"><mat-icon>delete</mat-icon></button>
55
+ <button matIconButton (click)="deleteRow(record)" title="Delete"><mat-icon>delete</mat-icon></button>
56
+ }
57
+ @if (record[config.opField] !== 'I') {
58
+ @for (action of getActions(true); track action.action) {
59
+ @if (canExecuteAction(action)) {
60
+ <button matIconButton type="button"
61
+ [title]="action.caption"
62
+ (click)="executeAction(action, record)">
63
+ <mat-icon>{{ action.icon || 'play_arrow' }}</mat-icon>
64
+ </button>
65
+ }
66
+ }
46
67
  }
47
68
  </td>
48
69
  }
@@ -1,10 +1,12 @@
1
- import { ChangeDetectionStrategy, Component, inject, Input, input, OnInit, ViewEncapsulation } from "@angular/core";
1
+ import { ChangeDetectionStrategy, Component, effect, inject, input, OnInit, ViewEncapsulation } from "@angular/core";
2
2
  import { DynamicField } from "../form/form_field";
3
3
  import { MatButtonModule } from "@angular/material/button";
4
4
  import { BaseForm } from "../abstract/base_form";
5
5
  import { MatDialog } from "@angular/material/dialog";
6
6
  import { TableForm } from "../form/form_table";
7
7
  import { MatIconModule } from "@angular/material/icon";
8
+ import { BackendService } from "../../service/rest_service";
9
+ import { TableAction } from "../../model/appdata";
8
10
 
9
11
  @Component({
10
12
  selector: 'table-detail',
@@ -18,19 +20,54 @@ import { MatIconModule } from "@angular/material/icon";
18
20
  ],
19
21
  })
20
22
  export class TableDetail extends BaseForm implements OnInit {
21
- @Input() override tableName = '';
22
- @Input() records: any[] = [];
23
+ readonly tableNameInput = input('', { alias: 'tableName' });
24
+ readonly recordsInput = input<any[]>([], { alias: 'records' });
23
25
  readonly parentTableName = input('');
24
26
  readonly parentRecord = input<any>({});
25
27
  /** When true, the parent view is in read-only mode — disable all child row editing. */
26
28
  readonly parentReadOnly = input(false);
27
29
 
30
+ records: any[] = [];
28
31
  editingRecord: any = null;
29
32
  originalRecord: any = null;
30
33
  displayedColumns: string[] = [];
31
34
  fkColumns: string[] = [];
32
35
 
33
36
  private readonly dialog = inject(MatDialog);
37
+ private readonly backendService = inject(BackendService);
38
+
39
+ constructor() {
40
+ super();
41
+ effect(() => { const v = this.tableNameInput(); if (v) this.tableName.set(v); });
42
+ effect(() => { this.records = this.recordsInput(); });
43
+ }
44
+
45
+ /**
46
+ * Fire a TableAction. Per-row actions require the record to be
47
+ * server-side (not 'I' inserted-locally-not-saved); table-level
48
+ * actions only need the parent to be writable. Action POSTs hit
49
+ * the resolved URL directly — no need to round-trip through the
50
+ * parent form.
51
+ */
52
+ executeAction(action: TableAction, record?: {[key: string]: unknown}): void {
53
+ if (!this.canExecuteAction(action)) {
54
+ alert(`Missing authorization for action ${action.authorityObject}/${action.authorityCheck}`);
55
+ return;
56
+ }
57
+ if (action.recordSpecific && record && record[this.config.opField] === 'I') {
58
+ alert('Save the record first before running this action.');
59
+ return;
60
+ }
61
+ if (action.confirmMessage && !confirm(action.confirmMessage)) return;
62
+ const body = action.recordSpecific && record ? this.primaryKeyValues(record) : {};
63
+ this.backendService.executeAction(action.method, body).subscribe({
64
+ next: () => {/* parent form re-fetches on its own save */},
65
+ error: (err) => {
66
+ console.error(`Action ${action.action} failed`, err);
67
+ alert(err?.error?.detail ?? `Failed to ${action.caption}`);
68
+ },
69
+ });
70
+ }
34
71
 
35
72
  override canCreate() { return !this.parentReadOnly() && super.canCreate(); }
36
73
  override canUpdate() { return !this.parentReadOnly() && super.canUpdate(); }
@@ -9,7 +9,7 @@
9
9
  }
10
10
  </span>
11
11
  @if (isDialog) {
12
- <button mat-icon-button type="button" class="table-edit-close"
12
+ <button matIconButton type="button" class="table-edit-close"
13
13
  (click)="onCancel()" aria-label="Close dialog">
14
14
  <mat-icon>close</mat-icon>
15
15
  </button>
@@ -20,21 +20,21 @@
20
20
  <div class="actions-bar">
21
21
  @if (isReadOnlyMode) {
22
22
  @if (canUpdate()) {
23
- <button mat-raised-button class="primary" (click)="onEdit()">Edit</button>
23
+ <button matButton="elevated" class="primary" (click)="onEdit()">Edit</button>
24
24
  }
25
25
  @if (canCreate()) {
26
- <button mat-raised-button class="accent" (click)="onNew()">New</button>
26
+ <button matButton="elevated" class="accent" (click)="onNew()">New</button>
27
27
  }
28
28
  @if (canDelete()) {
29
- <button mat-button class="warn" (click)="onDelete()">Delete</button>
29
+ <button matButton class="warn" (click)="onDelete()">Delete</button>
30
30
  }
31
31
  } @else {
32
- <button mat-raised-button class="primary" (click)="onSave()">Save</button>
33
- <button mat-button (click)="onCancel()">Cancel</button>
32
+ <button matButton="elevated" class="primary" (click)="onSave()">Save</button>
33
+ <button matButton (click)="onCancel()">Cancel</button>
34
34
  }
35
35
  </div>
36
36
 
37
- <record-form [tableName]="tableName" [record]="editableRecord" [isReadOnly]="isReadOnlyBound"></record-form>
37
+ <record-form [tableName]="tableName()" [record]="editableRecord" [isReadOnly]="isReadOnlyBound"></record-form>
38
38
 
39
39
  @if (tabCount > 0) {
40
40
  <mat-tab-group>
@@ -45,7 +45,7 @@
45
45
  <table-detail
46
46
  [records]="editableRecord[key]"
47
47
  [tableName]="getTableName(key)"
48
- [parentTableName]="tableName"
48
+ [parentTableName]="tableName()"
49
49
  [parentRecord]="editableRecord"
50
50
  [parentReadOnly]="isReadOnlyMode">
51
51
  </table-detail>
@@ -1,4 +1,4 @@
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 { BaseForm } from "../abstract/base_form";
3
3
  import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog";
4
4
  import { MatTabsModule } from "@angular/material/tabs";
@@ -22,9 +22,11 @@ import { MatIconModule } from "@angular/material/icon";
22
22
  ]
23
23
  })
24
24
  export class TableEdit extends BaseForm implements OnInit {
25
- @Input() record: any = {};
26
- @Input() override tableName = '';
27
- @Input() apiName = '';
25
+ readonly recordInput = input<any>({}, { alias: 'record' });
26
+ readonly tableNameInput = input('', { alias: 'tableName' });
27
+ readonly apiNameInput = input('', { alias: 'apiName' });
28
+ readonly record = signal<any>({});
29
+ readonly apiName = signal('');
28
30
  isReadOnlyMode = true;
29
31
  private readonly backendService = inject(BackendService);
30
32
  private readonly dialogRef = inject(MatDialogRef<TableEdit>, {optional: true});
@@ -37,11 +39,18 @@ export class TableEdit extends BaseForm implements OnInit {
37
39
  /** Snapshot taken when the user enters edit mode; used for dirty-tracking on save. */
38
40
  private originalSnapshot: any = null;
39
41
 
42
+ constructor() {
43
+ super();
44
+ effect(() => { const v = this.recordInput(); if (v) this.record.set(v); });
45
+ effect(() => { const v = this.tableNameInput(); if (v) this.tableName.set(v); });
46
+ effect(() => { const v = this.apiNameInput(); if (v) this.apiName.set(v); });
47
+ }
48
+
40
49
  ngOnInit(): void {
41
50
  if (this.dialogData) {
42
- this.record = this.dialogData.record ?? {};
43
- this.tableName = this.dialogData.tableName ?? '';
44
- this.apiName = this.dialogData.apiName ?? '';
51
+ this.record.set(this.dialogData.record ?? {});
52
+ this.tableName.set(this.dialogData.tableName ?? '');
53
+ this.apiName.set(this.dialogData.apiName ?? '');
45
54
  this.isNew = this.dialogData.isNew ?? false;
46
55
  this.isReadOnlyMode = !this.isNew;
47
56
  }
@@ -55,7 +64,8 @@ export class TableEdit extends BaseForm implements OnInit {
55
64
 
56
65
  private initializeNewRecord() {
57
66
  this.editableRecord = this.emptyRecord();
58
- if (this.record) Object.assign(this.editableRecord, this.record);
67
+ const rec = this.record();
68
+ if (rec) Object.assign(this.editableRecord, rec);
59
69
  }
60
70
 
61
71
  private buildTabs() {
@@ -70,16 +80,17 @@ export class TableEdit extends BaseForm implements OnInit {
70
80
  }
71
81
 
72
82
  private fetchFullRecord() {
73
- this.editableRecord = {...this.record};
83
+ const rec = this.record();
84
+ this.editableRecord = {...rec};
74
85
  this.formatRecordTimeStamp(this.editableRecord);
75
86
 
76
- const filter = this.getKeyFilters(this.record);
87
+ const filter = this.getKeyFilters(rec);
77
88
  if (!filter) {
78
89
  console.warn('Key definition could not be found');
79
90
  return;
80
91
  }
81
92
 
82
- this.backendService.get<any>(this.apiName, filter).subscribe({
93
+ this.backendService.get<any>(this.apiName(), filter).subscribe({
83
94
  next: (record) => {
84
95
  if (!record) {
85
96
  console.error('failed to read single record from the backend');
@@ -123,7 +134,7 @@ export class TableEdit extends BaseForm implements OnInit {
123
134
  if (this.dialogRef) {
124
135
  this.dialogRef.close(result);
125
136
  } else {
126
- this.backendService.post(this.apiName, result).subscribe({
137
+ this.backendService.post(this.apiName(), result).subscribe({
127
138
  next: () => {
128
139
  this.isReadOnlyMode = true;
129
140
  this.isNew = false;
@@ -139,7 +150,7 @@ export class TableEdit extends BaseForm implements OnInit {
139
150
  if (confirm('Are you sure you want to delete this record?')) {
140
151
  const result = {...this.editableRecord};
141
152
  result[this.config.opField] = 'D';
142
- this.backendService.post(this.apiName, result).subscribe({
153
+ this.backendService.post(this.apiName(), result).subscribe({
143
154
  next: () => {
144
155
  this.isReadOnlyMode = true;
145
156
  this.isReadOnlyBound = this.isReadOnly.bind(this);
@@ -156,7 +167,7 @@ export class TableEdit extends BaseForm implements OnInit {
156
167
  return;
157
168
  }
158
169
  this.isReadOnlyMode = true;
159
- this.editableRecord = {...this.record};
170
+ this.editableRecord = {...this.record()};
160
171
  this.isNew = false;
161
172
  this.isReadOnlyBound = this.isReadOnly.bind(this);
162
173
  }
@@ -1,8 +1,20 @@
1
1
  <h2>{{ getCaption() }}</h2>
2
2
 
3
- @if (canCreate()) {
4
- <button mat-raised-button class="accent" (click)="addRecord()">New Record</button>
5
- }
3
+ <div class="table-list__toolbar">
4
+ @if (canCreate()) {
5
+ <button matButton="elevated" class="accent" (click)="addRecord()">New Record</button>
6
+ }
7
+ @for (action of getActions(false); track action.action) {
8
+ @if (canExecuteAction(action)) {
9
+ <button matButton="outlined" type="button"
10
+ [title]="action.caption"
11
+ (click)="executeAction(action)">
12
+ @if (action.icon) { <mat-icon>{{ action.icon }}</mat-icon> }
13
+ {{ action.caption }}
14
+ </button>
15
+ }
16
+ }
17
+ </div>
6
18
 
7
19
  <table>
8
20
  <thead>
@@ -25,8 +37,17 @@
25
37
  } @else if (column !== 'actions') {
26
38
  {{ displayValue(column, record[column]) }}
27
39
  } @else {
28
- <button mat-icon-button type="button" (click)="editRecord(record)" title="Edit"><mat-icon>edit</mat-icon></button>
29
- <button mat-icon-button type="button" (click)="deleteRecord(record)" title="Delete"><mat-icon>delete</mat-icon></button>
40
+ <button matIconButton type="button" (click)="editRecord(record)" title="Edit"><mat-icon>edit</mat-icon></button>
41
+ <button matIconButton type="button" (click)="deleteRecord(record)" title="Delete"><mat-icon>delete</mat-icon></button>
42
+ @for (action of getActions(true); track action.action) {
43
+ @if (canExecuteAction(action)) {
44
+ <button matIconButton type="button"
45
+ [title]="action.caption"
46
+ (click)="executeAction(action, record)">
47
+ <mat-icon>{{ action.icon || 'play_arrow' }}</mat-icon>
48
+ </button>
49
+ }
50
+ }
30
51
  }
31
52
  </td>
32
53
  }
@@ -1,9 +1,10 @@
1
- import { ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject, OnInit, ViewEncapsulation } from "@angular/core";
1
+ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, effect, inject, input, OnInit, ViewEncapsulation } from "@angular/core";
2
2
  import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
3
3
  import { ActivatedRoute, Params } from "@angular/router";
4
4
  import { BaseView } from "../abstract/base_view";
5
5
  import { TableEdit } from "./table_edit";
6
6
  import { BackendService } from "../../service/rest_service";
7
+ import { TableAction } from "../../model/appdata";
7
8
  import { MatButtonModule } from "@angular/material/button";
8
9
  import { MatCheckboxModule } from "@angular/material/checkbox";
9
10
  import { MatIconModule } from "@angular/material/icon";
@@ -17,17 +18,27 @@ import { MatIconModule } from "@angular/material/icon";
17
18
  })
18
19
  export class TableList extends BaseView implements OnInit {
19
20
  override dialogWidth = '400px';
21
+ readonly tableNameInput = input('', { alias: 'tableName' });
22
+ readonly apiNameInput = input('', { alias: 'apiName' });
23
+ readonly dialogComponentInput = input<any>(undefined, { alias: 'dialogComponent' });
20
24
 
21
25
  private readonly route = inject(ActivatedRoute);
22
26
  private readonly backendService = inject(BackendService);
23
27
  private readonly cdr = inject(ChangeDetectorRef);
24
28
  private readonly destroyRef = inject(DestroyRef);
25
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.dialogComponentInput();if (v) this.dialogComponent.set(v); });
35
+ }
36
+
26
37
  ngOnInit() {
27
38
  const data = this.route.snapshot.data;
28
- if (!this.tableName && data['tableName']) this.tableName = data['tableName'];
29
- if (!this.apiName && data['apiName']) this.apiName = data['apiName'];
30
- if (!this.dialogComponent) this.dialogComponent = data['dialogComponent'] ?? TableEdit;
39
+ if (!this.tableName() && data['tableName']) this.tableName.set(data['tableName']);
40
+ if (!this.apiName() && data['apiName']) this.apiName.set(data['apiName']);
41
+ if (!this.dialogComponent()) this.dialogComponent.set(data['dialogComponent'] ?? TableEdit);
31
42
  this.route.queryParams.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((params) => this.handleQueryParams(params));
32
43
  }
33
44
 
@@ -38,7 +49,7 @@ export class TableList extends BaseView implements OnInit {
38
49
  }
39
50
  if (confirm('Are you sure you want to delete this record?')) {
40
51
  const keyFilter = this.getKeyFilters(record);
41
- this.backendService.delete(this.apiName, keyFilter).subscribe({
52
+ this.backendService.delete(this.apiName(), keyFilter).subscribe({
42
53
  next: () => this.fetchRecords(),
43
54
  error: (err) => console.error('Delete failed', err),
44
55
  });
@@ -55,7 +66,7 @@ export class TableList extends BaseView implements OnInit {
55
66
  return;
56
67
  }
57
68
  if (confirm('Are you sure you want to save this record?')) {
58
- this.backendService.post(this.apiName, result).subscribe({
69
+ this.backendService.post(this.apiName(), result).subscribe({
59
70
  next: () => this.fetchRecords(),
60
71
  error: (err) => console.error('Save failed', err),
61
72
  });
@@ -67,7 +78,7 @@ export class TableList extends BaseView implements OnInit {
67
78
  alert('Missing authorization to read records');
68
79
  return;
69
80
  }
70
- this.backendService.list<{[key: string]: any}>(this.apiName, this.searchTerms).subscribe({
81
+ this.backendService.list<{[key: string]: any}>(this.apiName(), this.searchTerms).subscribe({
71
82
  next: (records) => {
72
83
  this.records = records;
73
84
  this.updateDisplayedColumns(records);
@@ -77,6 +88,29 @@ export class TableList extends BaseView implements OnInit {
77
88
  });
78
89
  }
79
90
 
91
+ /**
92
+ * Fire a TableAction. For record-specific actions, `record` carries
93
+ * the row's primary key columns (lifted via BaseTable.primaryKeyValues).
94
+ * For table-level actions, `record` is undefined and the body is empty.
95
+ * Refreshes the row list on success — most actions mutate visible
96
+ * state (set_default flipping defaults, etc.).
97
+ */
98
+ executeAction(action: TableAction, record?: {[key: string]: unknown}): void {
99
+ if (!this.canExecuteAction(action)) {
100
+ alert(`Missing authorization for action ${action.authorityObject}/${action.authorityCheck}`);
101
+ return;
102
+ }
103
+ if (action.confirmMessage && !confirm(action.confirmMessage)) return;
104
+ const body = action.recordSpecific && record ? this.primaryKeyValues(record) : {};
105
+ this.backendService.executeAction(action.method, body).subscribe({
106
+ next: () => this.fetchRecords(),
107
+ error: (err) => {
108
+ console.error(`Action ${action.action} failed`, err);
109
+ alert(err?.error?.detail ?? `Failed to ${action.caption}`);
110
+ },
111
+ });
112
+ }
113
+
80
114
  handleQueryParams(params: Params) {
81
115
  const newSearchTerms: {[key: string]: string} = {};
82
116
  let hasSearchTerms = false;
@@ -1,9 +1,9 @@
1
1
  <h2 mat-dialog-title>{{ getCaption() }} Search</h2>
2
2
  <div mat-dialog-content>
3
- <record-form [tableName]="tableName" [record]="searchRecord" [fields]="searchColumns" [isReadOnly]="isReadOnlyBound"></record-form>
3
+ <record-form [tableName]="tableName()" [record]="searchRecord" [fields]="searchColumns" [isReadOnly]="isReadOnlyBound"></record-form>
4
4
  <div class="search-actions">
5
- <button mat-raised-button class="primary" (click)="onSearch()">Search</button>
6
- <button mat-button (click)="onClear()">Clear</button>
5
+ <button matButton="elevated" class="primary" (click)="onSearch()">Search</button>
6
+ <button matButton (click)="onClear()">Clear</button>
7
7
  </div>
8
8
  @if (records.length > 0) {
9
9
  <table>
@@ -31,5 +31,5 @@
31
31
  }
32
32
  </div>
33
33
  <div mat-dialog-actions>
34
- <button mat-button [mat-dialog-close]>Cancel</button>
34
+ <button matButton [mat-dialog-close]>Cancel</button>
35
35
  </div>
@@ -22,7 +22,6 @@ import { BackendService } from "../../service/rest_service";
22
22
  ],
23
23
  })
24
24
  export class TableLookup extends BaseView implements OnInit {
25
- override apiName = '';
26
25
  searchRecord: any = {};
27
26
  searchColumns: string[] = [];
28
27
 
@@ -33,8 +32,8 @@ export class TableLookup extends BaseView implements OnInit {
33
32
 
34
33
  ngOnInit() {
35
34
  if (this.data) {
36
- this.tableName = this.data.tableName ?? '';
37
- this.apiName = this.data.apiName ?? '';
35
+ this.tableName.set(this.data.tableName ?? '');
36
+ this.apiName.set(this.data.apiName ?? '');
38
37
  }
39
38
  this.searchColumns = this.getDisplayedColumns();
40
39
  this.searchRecord = this.emptyRecord();
@@ -42,13 +41,14 @@ export class TableLookup extends BaseView implements OnInit {
42
41
  }
43
42
 
44
43
  private resolveApi() {
45
- if (!this.apiName && this.tableName) {
44
+ const tableName = this.tableName();
45
+ if (!this.apiName() && tableName) {
46
46
  this.cacheService.getAppData().subscribe((data) => {
47
47
  if (data.Apis) {
48
48
  for (const api in data.Apis) {
49
49
  const entry = data.Apis[api];
50
- if (entry.Table.TableName === this.tableName) {
51
- this.apiName = entry.Version ? entry.Version + '/' + api : api;
50
+ if (entry.Table.TableName === tableName) {
51
+ this.apiName.set(entry.Version ? entry.Version + '/' + api : api);
52
52
  break;
53
53
  }
54
54
  }
@@ -59,11 +59,12 @@ export class TableLookup extends BaseView implements OnInit {
59
59
 
60
60
  onSearch() {
61
61
  this.resolveApi();
62
- if (!this.apiName) {
63
- console.error('API name not found for table ' + this.tableName);
62
+ const apiName = this.apiName();
63
+ if (!apiName) {
64
+ console.error('API name not found for table ' + this.tableName());
64
65
  return;
65
66
  }
66
- this.backendService.list<{[key: string]: any}>(this.apiName, this.buildSearchTerms(this.searchRecord)).subscribe({
67
+ this.backendService.list<{[key: string]: any}>(apiName, this.buildSearchTerms(this.searchRecord)).subscribe({
67
68
  next: (records) => {
68
69
  this.records = records;
69
70
  this.updateDisplayedColumns(records);
@@ -27,7 +27,7 @@
27
27
  </mat-form-field>
28
28
  }
29
29
  }
30
- <button mat-raised-button color="primary" type="submit">Run</button>
30
+ <button matButton="elevated" class="primary" type="submit">Run</button>
31
31
  </form>
32
32
  }
33
33
 
@@ -6,12 +6,22 @@
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
+ <record-form [tableName]="tableName()" [record]="editableRecord" [fields]="searchColumns" [isReadOnly]="isReadOnlyBound"></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
+ }
17
+ @for (action of getActions(false); track action.action) {
18
+ @if (canExecuteAction(action)) {
19
+ <button matButton="outlined" type="button"
20
+ [title]="action.caption"
21
+ (click)="executeAction(action)">
22
+ @if (action.icon) { <mat-icon>{{ action.icon }}</mat-icon> }
23
+ {{ action.caption }}
24
+ </button>
25
+ }
16
26
  }
17
27
  </div>