@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
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
32
|
-
<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
|
|
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
|
|
53
|
+
<button matIconButton (click)="unDeleteRow(record)" title="Undelete"><mat-icon>restore_from_trash</mat-icon></button>
|
|
44
54
|
} @else if (canDelete()) {
|
|
45
|
-
<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,
|
|
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
|
-
|
|
22
|
-
|
|
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
|
|
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
|
|
23
|
+
<button matButton="elevated" class="primary" (click)="onEdit()">Edit</button>
|
|
24
24
|
}
|
|
25
25
|
@if (canCreate()) {
|
|
26
|
-
<button
|
|
26
|
+
<button matButton="elevated" class="accent" (click)="onNew()">New</button>
|
|
27
27
|
}
|
|
28
28
|
@if (canDelete()) {
|
|
29
|
-
<button
|
|
29
|
+
<button matButton class="warn" (click)="onDelete()">Delete</button>
|
|
30
30
|
}
|
|
31
31
|
} @else {
|
|
32
|
-
<button
|
|
33
|
-
<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,
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
43
|
-
this.tableName
|
|
44
|
-
this.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
|
-
|
|
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
|
-
|
|
83
|
+
const rec = this.record();
|
|
84
|
+
this.editableRecord = {...rec};
|
|
74
85
|
this.formatRecordTimeStamp(this.editableRecord);
|
|
75
86
|
|
|
76
|
-
const filter = this.getKeyFilters(
|
|
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
|
-
|
|
4
|
-
|
|
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
|
|
29
|
-
<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
|
|
29
|
-
if (!this.apiName && data['apiName']) this.apiName
|
|
30
|
-
if (!this.dialogComponent) this.dialogComponent
|
|
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
|
|
6
|
-
<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
|
|
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
|
|
37
|
-
this.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
|
-
|
|
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 ===
|
|
51
|
-
this.apiName
|
|
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
|
-
|
|
63
|
-
|
|
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}>(
|
|
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);
|
|
@@ -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
|
|
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
|
+
}
|
|
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>
|