@nauticana/sail 0.7.0 → 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 (39) hide show
  1. package/README.md +300 -1
  2. package/package.json +2 -2
  3. package/src/component/abstract/base_table.ts +19 -19
  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 +9 -9
  30. package/src/component/table/table_detail.ts +10 -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 +5 -5
  34. package/src/component/table/table_list.ts +17 -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 +5 -5
  39. package/src/component/table/table_search.ts +30 -17
@@ -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,11 +2,11 @@
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
6
  }
7
7
  @for (action of getActions(false); track action.action) {
8
8
  @if (canExecuteAction(action)) {
9
- <button mat-stroked-button type="button"
9
+ <button matButton="outlined" type="button"
10
10
  [title]="action.caption"
11
11
  (click)="executeAction(action)">
12
12
  @if (action.icon) { <mat-icon>{{ action.icon }}</mat-icon> }
@@ -33,13 +33,13 @@
33
33
  @if (isReadOnly(col)) {
34
34
  {{ record[col] }}
35
35
  } @else {
36
- <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>
37
37
  }
38
38
  </td>
39
39
  }
40
40
  <td>
41
- <button mat-icon-button (click)="saveRow()" title="Save"><mat-icon>save</mat-icon></button>
42
- <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>
43
43
  </td>
44
44
  } @else {
45
45
  @for (col of displayedColumns; track col) {
@@ -47,17 +47,17 @@
47
47
  }
48
48
  <td>
49
49
  @if (canUpdate()) {
50
- <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>
51
51
  }
52
52
  @if (record[config.opField] === 'D') {
53
- <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>
54
54
  } @else if (canDelete()) {
55
- <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
56
  }
57
57
  @if (record[config.opField] !== 'I') {
58
58
  @for (action of getActions(true); track action.action) {
59
59
  @if (canExecuteAction(action)) {
60
- <button mat-icon-button type="button"
60
+ <button matIconButton type="button"
61
61
  [title]="action.caption"
62
62
  (click)="executeAction(action, record)">
63
63
  <mat-icon>{{ action.icon || 'play_arrow' }}</mat-icon>
@@ -1,4 +1,4 @@
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";
@@ -20,13 +20,14 @@ import { TableAction } from "../../model/appdata";
20
20
  ],
21
21
  })
22
22
  export class TableDetail extends BaseForm implements OnInit {
23
- @Input() override tableName = '';
24
- @Input() records: any[] = [];
23
+ readonly tableNameInput = input('', { alias: 'tableName' });
24
+ readonly recordsInput = input<any[]>([], { alias: 'records' });
25
25
  readonly parentTableName = input('');
26
26
  readonly parentRecord = input<any>({});
27
27
  /** When true, the parent view is in read-only mode — disable all child row editing. */
28
28
  readonly parentReadOnly = input(false);
29
29
 
30
+ records: any[] = [];
30
31
  editingRecord: any = null;
31
32
  originalRecord: any = null;
32
33
  displayedColumns: string[] = [];
@@ -35,6 +36,12 @@ export class TableDetail extends BaseForm implements OnInit {
35
36
  private readonly dialog = inject(MatDialog);
36
37
  private readonly backendService = inject(BackendService);
37
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
+
38
45
  /**
39
46
  * Fire a TableAction. Per-row actions require the record to be
40
47
  * server-side (not 'I' inserted-locally-not-saved); table-level
@@ -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
  }
@@ -2,11 +2,11 @@
2
2
 
3
3
  <div class="table-list__toolbar">
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
6
  }
7
7
  @for (action of getActions(false); track action.action) {
8
8
  @if (canExecuteAction(action)) {
9
- <button mat-stroked-button type="button"
9
+ <button matButton="outlined" type="button"
10
10
  [title]="action.caption"
11
11
  (click)="executeAction(action)">
12
12
  @if (action.icon) { <mat-icon>{{ action.icon }}</mat-icon> }
@@ -37,11 +37,11 @@
37
37
  } @else if (column !== 'actions') {
38
38
  {{ displayValue(column, record[column]) }}
39
39
  } @else {
40
- <button mat-icon-button type="button" (click)="editRecord(record)" title="Edit"><mat-icon>edit</mat-icon></button>
41
- <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
42
  @for (action of getActions(true); track action.action) {
43
43
  @if (canExecuteAction(action)) {
44
- <button mat-icon-button type="button"
44
+ <button matIconButton type="button"
45
45
  [title]="action.caption"
46
46
  (click)="executeAction(action, record)">
47
47
  <mat-icon>{{ action.icon || 'play_arrow' }}</mat-icon>
@@ -1,4 +1,4 @@
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";
@@ -18,17 +18,27 @@ import { MatIconModule } from "@angular/material/icon";
18
18
  })
19
19
  export class TableList extends BaseView implements OnInit {
20
20
  override dialogWidth = '400px';
21
+ readonly tableNameInput = input('', { alias: 'tableName' });
22
+ readonly apiNameInput = input('', { alias: 'apiName' });
23
+ readonly dialogComponentInput = input<any>(undefined, { alias: 'dialogComponent' });
21
24
 
22
25
  private readonly route = inject(ActivatedRoute);
23
26
  private readonly backendService = inject(BackendService);
24
27
  private readonly cdr = inject(ChangeDetectorRef);
25
28
  private readonly destroyRef = inject(DestroyRef);
26
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
+
27
37
  ngOnInit() {
28
38
  const data = this.route.snapshot.data;
29
- if (!this.tableName && data['tableName']) this.tableName = data['tableName'];
30
- if (!this.apiName && data['apiName']) this.apiName = data['apiName'];
31
- 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);
32
42
  this.route.queryParams.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((params) => this.handleQueryParams(params));
33
43
  }
34
44
 
@@ -39,7 +49,7 @@ export class TableList extends BaseView implements OnInit {
39
49
  }
40
50
  if (confirm('Are you sure you want to delete this record?')) {
41
51
  const keyFilter = this.getKeyFilters(record);
42
- this.backendService.delete(this.apiName, keyFilter).subscribe({
52
+ this.backendService.delete(this.apiName(), keyFilter).subscribe({
43
53
  next: () => this.fetchRecords(),
44
54
  error: (err) => console.error('Delete failed', err),
45
55
  });
@@ -56,7 +66,7 @@ export class TableList extends BaseView implements OnInit {
56
66
  return;
57
67
  }
58
68
  if (confirm('Are you sure you want to save this record?')) {
59
- this.backendService.post(this.apiName, result).subscribe({
69
+ this.backendService.post(this.apiName(), result).subscribe({
60
70
  next: () => this.fetchRecords(),
61
71
  error: (err) => console.error('Save failed', err),
62
72
  });
@@ -68,7 +78,7 @@ export class TableList extends BaseView implements OnInit {
68
78
  alert('Missing authorization to read records');
69
79
  return;
70
80
  }
71
- this.backendService.list<{[key: string]: any}>(this.apiName, this.searchTerms).subscribe({
81
+ this.backendService.list<{[key: string]: any}>(this.apiName(), this.searchTerms).subscribe({
72
82
  next: (records) => {
73
83
  this.records = records;
74
84
  this.updateDisplayedColumns(records);
@@ -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,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
+ <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
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,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 { MatButtonModule } from "@angular/material/button";
3
3
  import { MatIconModule } from "@angular/material/icon";
4
4
  import { ActivatedRoute, Router } from "@angular/router";
@@ -20,11 +20,20 @@ import { BackendService } from "../../service/rest_service";
20
20
  ],
21
21
  })
22
22
  export class TableSearch extends BaseForm implements OnInit {
23
- @Input() override tableName = '';
24
- @Input() apiName: string = '';
25
- @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('');
26
28
  searchColumns: string[] = [];
27
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
+
28
37
  private readonly router = inject(Router);
29
38
  private readonly route = inject(ActivatedRoute);
30
39
  private readonly cdr = inject(ChangeDetectorRef);
@@ -52,21 +61,23 @@ export class TableSearch extends BaseForm implements OnInit {
52
61
 
53
62
  ngOnInit() {
54
63
  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'];
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']);
58
67
  this.title = this.getCaption() + ' Search';
59
- if (!this.targetRoute && (this.apiName || this.tableName)) {
68
+ if (!this.targetRoute() && (this.apiName() || this.tableName())) {
60
69
  this.cacheService.getMenus().subscribe((menus: ApplicationMenu[]) => {
70
+ const apiName = this.apiName();
71
+ const tableName = this.tableName();
61
72
  for (const menu of menus) {
62
73
  for (const page of menu.ApplicationMenuItems!) {
63
- if (this.apiName.endsWith(page.RestUri!) ||
64
- (this.tableName && page.RestUri!.includes(this.tableName))
74
+ if (apiName.endsWith(page.RestUri!) ||
75
+ (tableName && page.RestUri!.includes(tableName))
65
76
  ) {
66
77
  if (page.FilterOnList) {
67
- this.targetRoute = '/' + menu.Id!.toLowerCase() + '/' + page.ItemId!;
78
+ this.targetRoute.set('/' + menu.Id!.toLowerCase() + '/' + page.ItemId!);
68
79
  } else {
69
- this.targetRoute = '/' + menu.Id!.toLowerCase() + '/' + page.ItemId! + '/list';
80
+ this.targetRoute.set('/' + menu.Id!.toLowerCase() + '/' + page.ItemId! + '/list');
70
81
  }
71
82
  this.cdr.markForCheck();
72
83
  return;
@@ -80,11 +91,12 @@ export class TableSearch extends BaseForm implements OnInit {
80
91
  }
81
92
 
82
93
  onSearch() {
83
- if (!this.targetRoute) {
94
+ const targetRoute = this.targetRoute();
95
+ if (!targetRoute) {
84
96
  alert('Navigation target not found for this table');
85
97
  return;
86
98
  }
87
- this.router.navigate([this.targetRoute], {queryParams: this.buildSearchTerms(this.editableRecord)});
99
+ this.router.navigate([targetRoute], {queryParams: this.buildSearchTerms(this.editableRecord)});
88
100
  }
89
101
 
90
102
  onClear() {
@@ -106,7 +118,7 @@ export class TableSearch extends BaseForm implements OnInit {
106
118
  */
107
119
  private emptySearchRecord(): any {
108
120
  const rec = this.emptyRecord();
109
- const tableDef = this.cacheService.getTableDefinition(this.tableName);
121
+ const tableDef = this.cacheService.getTableDefinition(this.tableName());
110
122
  if (tableDef?.Columns) {
111
123
  for (const col of tableDef.Columns) {
112
124
  if (col.DataType === 'boolean') {
@@ -122,10 +134,11 @@ export class TableSearch extends BaseForm implements OnInit {
122
134
  alert('Missing authorization to create records');
123
135
  return;
124
136
  }
125
- if (!this.targetRoute) {
137
+ const targetRoute = this.targetRoute();
138
+ if (!targetRoute) {
126
139
  alert('Navigation target not found for this table');
127
140
  return;
128
141
  }
129
- this.router.navigate([this.targetRoute], {queryParams: {'_action': 'create'}});
142
+ this.router.navigate([targetRoute], {queryParams: {'_action': 'create'}});
130
143
  }
131
144
  }