@nauticana/sail 0.7.0 → 0.8.1
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 +419 -15
- package/package.json +2 -2
- package/src/component/abstract/base_async.ts +8 -2
- package/src/component/abstract/base_table.ts +108 -49
- package/src/component/abstract/base_view.ts +32 -45
- package/src/component/billing/checkout_button.html +1 -1
- package/src/component/billing/checkout_button.ts +1 -1
- package/src/component/billing/payment_methods.ts +1 -1
- package/src/component/billing/plan_selector.html +1 -1
- package/src/component/billing/plan_selector.ts +1 -1
- package/src/component/form/form_field.html +1 -1
- package/src/component/form/form_field.ts +8 -7
- package/src/component/form/form_record.html +2 -2
- package/src/component/form/form_record.ts +1 -1
- package/src/component/form/form_table.html +4 -4
- package/src/component/form/form_table.ts +2 -2
- package/src/component/login/account_deletion.html +1 -1
- package/src/component/login/account_deletion.ts +5 -2
- package/src/component/login/chpass_component.html +1 -1
- package/src/component/login/chpass_component.ts +11 -16
- package/src/component/login/confirm_chpass_component.html +1 -1
- package/src/component/login/confirm_chpass_component.ts +9 -15
- package/src/component/login/confirm_register_component.html +1 -1
- package/src/component/login/confirm_register_component.ts +9 -15
- package/src/component/login/consent_gate.ts +1 -1
- package/src/component/login/login_component.html +2 -2
- package/src/component/login/login_component.ts +1 -1
- package/src/component/login/otp_input.html +3 -3
- package/src/component/login/otp_input.ts +1 -1
- package/src/component/login/register_component.html +1 -1
- package/src/component/login/register_component.ts +1 -1
- package/src/component/login/social_login.ts +1 -1
- package/src/component/navigation/navigation.html +3 -3
- package/src/component/navigation/navigation.ts +10 -11
- 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/trusted_devices.ts +1 -1
- package/src/component/security/twofactor_setup.html +4 -4
- package/src/component/security/twofactor_setup.ts +18 -26
- package/src/component/security/twofactor_verify.html +1 -1
- package/src/component/security/twofactor_verify.ts +10 -12
- package/src/component/table/table_detail.html +12 -12
- package/src/component/table/table_detail.ts +28 -33
- package/src/component/table/table_edit.html +20 -10
- package/src/component/table/table_edit.ts +29 -18
- package/src/component/table/table_list.html +5 -5
- package/src/component/table/table_list.ts +24 -48
- package/src/component/table/table_lookup.html +4 -4
- package/src/component/table/table_lookup.ts +11 -12
- package/src/component/table/table_report.html +1 -1
- package/src/component/table/table_report.ts +11 -14
- package/src/component/table/table_search.html +5 -5
- package/src/component/table/table_search.ts +31 -42
- package/src/index.ts +2 -0
- package/src/model/appdata.ts +2 -2
- package/src/model/common.ts +39 -1
- package/src/service/auth.service.ts +26 -26
- package/src/service/base_rest.service.ts +20 -0
- package/src/service/billing.service.ts +3 -9
- package/src/service/payout.service.ts +3 -9
- package/src/service/rest_service.ts +5 -5
- package/src/service/user_payment_method.service.ts +4 -8
- package/src/util/text.ts +19 -0
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { ChangeDetectionStrategy, Component,
|
|
1
|
+
import { ChangeDetectionStrategy, Component, effect, inject, input, linkedSignal, 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
8
|
import { TableAction } from "../../model/appdata";
|
|
9
|
+
import { OpCode } from "../../model/common";
|
|
10
10
|
|
|
11
11
|
@Component({
|
|
12
|
-
selector: 'table-detail',
|
|
12
|
+
selector: 'sail-table-detail',
|
|
13
13
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
14
14
|
encapsulation: ViewEncapsulation.None,
|
|
15
15
|
templateUrl: './table_detail.html',
|
|
@@ -20,46 +20,41 @@ import { TableAction } from "../../model/appdata";
|
|
|
20
20
|
],
|
|
21
21
|
})
|
|
22
22
|
export class TableDetail extends BaseForm implements OnInit {
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
protected readonly tableNameInput = input('', { alias: 'tableName' });
|
|
24
|
+
protected 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
|
+
override readonly tableName = linkedSignal<string, string>({ source: () => this.tableNameInput(), computation: (v, p) => v || p?.value || '' });
|
|
31
|
+
|
|
32
|
+
records: any[] = [];
|
|
30
33
|
editingRecord: any = null;
|
|
31
34
|
originalRecord: any = null;
|
|
32
35
|
displayedColumns: string[] = [];
|
|
33
36
|
fkColumns: string[] = [];
|
|
34
37
|
|
|
35
38
|
private readonly dialog = inject(MatDialog);
|
|
36
|
-
|
|
39
|
+
|
|
40
|
+
constructor() {
|
|
41
|
+
super();
|
|
42
|
+
// records is a writable array (push/splice in event handlers); keep it in sync with the input
|
|
43
|
+
// via effect rather than linkedSignal so mutations work on the consumer's array reference.
|
|
44
|
+
effect(() => { this.records = this.recordsInput(); });
|
|
45
|
+
}
|
|
37
46
|
|
|
38
47
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* the resolved URL directly — no need to round-trip through the
|
|
43
|
-
* parent form.
|
|
48
|
+
* Per-row actions require the record to be server-side (not 'I'
|
|
49
|
+
* inserted-locally-not-saved); the keel action POST would hit a record
|
|
50
|
+
* that doesn't exist yet. Table-level actions are always allowed.
|
|
44
51
|
*/
|
|
45
|
-
|
|
46
|
-
if (
|
|
47
|
-
alert(`Missing authorization for action ${action.authorityObject}/${action.authorityCheck}`);
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
if (action.recordSpecific && record && record[this.config.opField] === 'I') {
|
|
52
|
+
protected override beforeExecuteAction(action: TableAction, record?: Record<string, unknown>): boolean {
|
|
53
|
+
if (action.recordSpecific && record && record[this.config.opField] === OpCode.Insert) {
|
|
51
54
|
alert('Save the record first before running this action.');
|
|
52
|
-
return;
|
|
55
|
+
return false;
|
|
53
56
|
}
|
|
54
|
-
|
|
55
|
-
const body = action.recordSpecific && record ? this.primaryKeyValues(record) : {};
|
|
56
|
-
this.backendService.executeAction(action.method, body).subscribe({
|
|
57
|
-
next: () => {/* parent form re-fetches on its own save */},
|
|
58
|
-
error: (err) => {
|
|
59
|
-
console.error(`Action ${action.action} failed`, err);
|
|
60
|
-
alert(err?.error?.detail ?? `Failed to ${action.caption}`);
|
|
61
|
-
},
|
|
62
|
-
});
|
|
57
|
+
return true;
|
|
63
58
|
}
|
|
64
59
|
|
|
65
60
|
override canCreate() { return !this.parentReadOnly() && super.canCreate(); }
|
|
@@ -99,7 +94,7 @@ export class TableDetail extends BaseForm implements OnInit {
|
|
|
99
94
|
|
|
100
95
|
addRecord() {
|
|
101
96
|
const newRecord = this.emptyRecord();
|
|
102
|
-
newRecord[this.config.opField] =
|
|
97
|
+
newRecord[this.config.opField] = OpCode.Insert;
|
|
103
98
|
this.initializeForeignKeys(newRecord, this.parentTableName(), this.parentRecord());
|
|
104
99
|
|
|
105
100
|
if (this.displayedColumns.length > 6) {
|
|
@@ -117,7 +112,7 @@ export class TableDetail extends BaseForm implements OnInit {
|
|
|
117
112
|
this.openEditDialog(record, false, this.fkColumns);
|
|
118
113
|
} else {
|
|
119
114
|
this.editingRecord = record;
|
|
120
|
-
this.isNew = record[this.config.opField] ===
|
|
115
|
+
this.isNew = record[this.config.opField] === OpCode.Insert;
|
|
121
116
|
this.originalRecord = {...record};
|
|
122
117
|
this.formatRecordTimeStamp(record);
|
|
123
118
|
}
|
|
@@ -145,19 +140,19 @@ export class TableDetail extends BaseForm implements OnInit {
|
|
|
145
140
|
}
|
|
146
141
|
|
|
147
142
|
deleteRow(record: any) {
|
|
148
|
-
if (record[this.config.opField] ===
|
|
143
|
+
if (record[this.config.opField] === OpCode.Insert) {
|
|
149
144
|
const index = this.records.indexOf(record);
|
|
150
145
|
if (index > -1) {
|
|
151
146
|
this.records.splice(index, 1);
|
|
152
147
|
}
|
|
153
148
|
} else {
|
|
154
|
-
record[this.config.opField] =
|
|
149
|
+
record[this.config.opField] = OpCode.Delete;
|
|
155
150
|
}
|
|
156
151
|
}
|
|
157
152
|
|
|
158
153
|
unDeleteRow(record: any) {
|
|
159
|
-
if (record[this.config.opField] ===
|
|
160
|
-
record[this.config.opField] =
|
|
154
|
+
if (record[this.config.opField] === OpCode.Delete) {
|
|
155
|
+
record[this.config.opField] = OpCode.Update;
|
|
161
156
|
}
|
|
162
157
|
}
|
|
163
158
|
}
|
|
@@ -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,31 @@
|
|
|
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
|
+
}
|
|
31
|
+
@for (action of getActions(true); track action.action) {
|
|
32
|
+
@if (canExecuteAction(action)) {
|
|
33
|
+
<button matButton="outlined" type="button"
|
|
34
|
+
[title]="action.caption"
|
|
35
|
+
(click)="executeAction(action, record())">
|
|
36
|
+
@if (action.icon) { <mat-icon>{{ action.icon }}</mat-icon> }
|
|
37
|
+
{{ action.caption }}
|
|
38
|
+
</button>
|
|
39
|
+
}
|
|
30
40
|
}
|
|
31
41
|
} @else {
|
|
32
|
-
<button
|
|
33
|
-
<button
|
|
42
|
+
<button matButton="elevated" class="primary" (click)="onSave()">Save</button>
|
|
43
|
+
<button matButton (click)="onCancel()">Cancel</button>
|
|
34
44
|
}
|
|
35
45
|
</div>
|
|
36
46
|
|
|
37
|
-
<record-form [tableName]="tableName" [record]="editableRecord" [isReadOnly]="isReadOnlyBound"></record-form>
|
|
47
|
+
<sail-record-form [tableName]="tableName()" [record]="editableRecord" [isReadOnly]="isReadOnlyBound"></sail-record-form>
|
|
38
48
|
|
|
39
49
|
@if (tabCount > 0) {
|
|
40
50
|
<mat-tab-group>
|
|
@@ -42,13 +52,13 @@
|
|
|
42
52
|
@if (cacheService.canRead(getTableName(key))) {
|
|
43
53
|
<mat-tab [label]="key">
|
|
44
54
|
<div class="tab-content">
|
|
45
|
-
<table-detail
|
|
55
|
+
<sail-table-detail
|
|
46
56
|
[records]="editableRecord[key]"
|
|
47
57
|
[tableName]="getTableName(key)"
|
|
48
|
-
[parentTableName]="tableName"
|
|
58
|
+
[parentTableName]="tableName()"
|
|
49
59
|
[parentRecord]="editableRecord"
|
|
50
60
|
[parentReadOnly]="isReadOnlyMode">
|
|
51
|
-
</table-detail>
|
|
61
|
+
</sail-table-detail>
|
|
52
62
|
</div>
|
|
53
63
|
</mat-tab>
|
|
54
64
|
}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, inject,
|
|
1
|
+
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, input, linkedSignal, OnInit, 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";
|
|
5
5
|
import { TableDetail } from "./table_detail";
|
|
6
|
-
import { BackendService } from "../../service/rest_service";
|
|
7
6
|
import { RecordForm } from "../form/form_record";
|
|
8
7
|
import { MatButtonModule } from "@angular/material/button";
|
|
9
8
|
import { MatIconModule } from "@angular/material/icon";
|
|
9
|
+
import { OpCode } from "../../model/common";
|
|
10
10
|
|
|
11
11
|
@Component({
|
|
12
|
-
selector: 'table-edit',
|
|
12
|
+
selector: 'sail-table-edit',
|
|
13
13
|
templateUrl: './table_edit.html',
|
|
14
14
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
15
15
|
encapsulation: ViewEncapsulation.None,
|
|
@@ -22,11 +22,15 @@ import { MatIconModule } from "@angular/material/icon";
|
|
|
22
22
|
]
|
|
23
23
|
})
|
|
24
24
|
export class TableEdit extends BaseForm implements OnInit {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
protected readonly recordInput = input<any>({}, { alias: 'record' });
|
|
26
|
+
protected readonly tableNameInput = input('', { alias: 'tableName' });
|
|
27
|
+
protected readonly apiNameInput = input('', { alias: 'apiName' });
|
|
28
|
+
|
|
29
|
+
readonly record = linkedSignal<any, any>({ source: () => this.recordInput(), computation: (v, p) => v ?? p?.value ?? {} });
|
|
30
|
+
override readonly tableName = linkedSignal<string, string>({ source: () => this.tableNameInput(), computation: (v, p) => v || p?.value || '' });
|
|
31
|
+
readonly apiName = linkedSignal<string, string>({ source: () => this.apiNameInput(), computation: (v, p) => v || p?.value || '' });
|
|
32
|
+
|
|
28
33
|
isReadOnlyMode = true;
|
|
29
|
-
private readonly backendService = inject(BackendService);
|
|
30
34
|
private readonly dialogRef = inject(MatDialogRef<TableEdit>, {optional: true});
|
|
31
35
|
private readonly dialogData = inject<{record?: any; tableName?: string; apiName?: string; isNew?: boolean} | null>(MAT_DIALOG_DATA, {optional: true});
|
|
32
36
|
private readonly cdr = inject(ChangeDetectorRef);
|
|
@@ -39,9 +43,9 @@ export class TableEdit extends BaseForm implements OnInit {
|
|
|
39
43
|
|
|
40
44
|
ngOnInit(): void {
|
|
41
45
|
if (this.dialogData) {
|
|
42
|
-
this.record
|
|
43
|
-
this.tableName
|
|
44
|
-
this.apiName
|
|
46
|
+
this.record.set(this.dialogData.record ?? {});
|
|
47
|
+
this.tableName.set(this.dialogData.tableName ?? '');
|
|
48
|
+
this.apiName.set(this.dialogData.apiName ?? '');
|
|
45
49
|
this.isNew = this.dialogData.isNew ?? false;
|
|
46
50
|
this.isReadOnlyMode = !this.isNew;
|
|
47
51
|
}
|
|
@@ -55,7 +59,8 @@ export class TableEdit extends BaseForm implements OnInit {
|
|
|
55
59
|
|
|
56
60
|
private initializeNewRecord() {
|
|
57
61
|
this.editableRecord = this.emptyRecord();
|
|
58
|
-
|
|
62
|
+
const rec = this.record();
|
|
63
|
+
if (rec) Object.assign(this.editableRecord, rec);
|
|
59
64
|
}
|
|
60
65
|
|
|
61
66
|
private buildTabs() {
|
|
@@ -69,17 +74,23 @@ export class TableEdit extends BaseForm implements OnInit {
|
|
|
69
74
|
}
|
|
70
75
|
}
|
|
71
76
|
|
|
77
|
+
/** Re-fetch the row from the backend after a server-side mutation (TableAction success, etc.). */
|
|
78
|
+
protected override onActionSuccess(): void {
|
|
79
|
+
if (!this.isNew) this.fetchFullRecord();
|
|
80
|
+
}
|
|
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;
|
|
@@ -138,8 +149,8 @@ export class TableEdit extends BaseForm implements OnInit {
|
|
|
138
149
|
onDelete() {
|
|
139
150
|
if (confirm('Are you sure you want to delete this record?')) {
|
|
140
151
|
const result = {...this.editableRecord};
|
|
141
|
-
result[this.config.opField] =
|
|
142
|
-
this.backendService.post(this.apiName, result).subscribe({
|
|
152
|
+
result[this.config.opField] = OpCode.Delete;
|
|
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
|
|
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
|
|
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
|
|
41
|
-
<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
|
|
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,16 +1,14 @@
|
|
|
1
|
-
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject, OnInit, ViewEncapsulation } from "@angular/core";
|
|
1
|
+
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject, input, linkedSignal, 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
|
-
import { BackendService } from "../../service/rest_service";
|
|
7
|
-
import { TableAction } from "../../model/appdata";
|
|
8
6
|
import { MatButtonModule } from "@angular/material/button";
|
|
9
7
|
import { MatCheckboxModule } from "@angular/material/checkbox";
|
|
10
8
|
import { MatIconModule } from "@angular/material/icon";
|
|
11
9
|
|
|
12
10
|
@Component({
|
|
13
|
-
selector: 'table-list',
|
|
11
|
+
selector: 'sail-table-list',
|
|
14
12
|
templateUrl: './table_list.html',
|
|
15
13
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
16
14
|
encapsulation: ViewEncapsulation.None,
|
|
@@ -18,28 +16,34 @@ import { MatIconModule } from "@angular/material/icon";
|
|
|
18
16
|
})
|
|
19
17
|
export class TableList extends BaseView implements OnInit {
|
|
20
18
|
override dialogWidth = '400px';
|
|
19
|
+
protected readonly tableNameInput = input('', { alias: 'tableName' });
|
|
20
|
+
protected readonly apiNameInput = input('', { alias: 'apiName' });
|
|
21
|
+
protected readonly dialogComponentInput = input<any>(undefined, { alias: 'dialogComponent' });
|
|
22
|
+
|
|
23
|
+
// linkedSignal: defaults to the consumer's input, but ngOnInit / route data
|
|
24
|
+
// can override with `.set()`. The `computation` preserves the last
|
|
25
|
+
// non-empty value when the input transiently becomes empty.
|
|
26
|
+
override readonly tableName = linkedSignal<string, string>({ source: () => this.tableNameInput(), computation: (v, p) => v || p?.value || '' });
|
|
27
|
+
override readonly apiName = linkedSignal<string, string>({ source: () => this.apiNameInput(), computation: (v, p) => v || p?.value || '' });
|
|
28
|
+
override readonly dialogComponent = linkedSignal<any, any>({ source: () => this.dialogComponentInput(), computation: (v, p) => v ?? p?.value });
|
|
21
29
|
|
|
22
30
|
private readonly route = inject(ActivatedRoute);
|
|
23
|
-
private readonly backendService = inject(BackendService);
|
|
24
31
|
private readonly cdr = inject(ChangeDetectorRef);
|
|
25
32
|
private readonly destroyRef = inject(DestroyRef);
|
|
26
33
|
|
|
27
34
|
ngOnInit() {
|
|
28
35
|
const data = this.route.snapshot.data;
|
|
29
|
-
if (!this.tableName && data['tableName']) this.tableName
|
|
30
|
-
if (!this.apiName && data['apiName']) this.apiName
|
|
31
|
-
if (!this.dialogComponent) this.dialogComponent
|
|
36
|
+
if (!this.tableName() && data['tableName']) this.tableName.set(data['tableName']);
|
|
37
|
+
if (!this.apiName() && data['apiName']) this.apiName.set(data['apiName']);
|
|
38
|
+
if (!this.dialogComponent()) this.dialogComponent.set(data['dialogComponent'] ?? TableEdit);
|
|
32
39
|
this.route.queryParams.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((params) => this.handleQueryParams(params));
|
|
33
40
|
}
|
|
34
41
|
|
|
35
42
|
protected handleDelete(record: {[key: string]: any}): void {
|
|
36
|
-
if (!this.canDelete())
|
|
37
|
-
alert('Missing authorization to delete records');
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
43
|
+
if (!this.requireAuth(() => this.canDelete(), 'delete')) return;
|
|
40
44
|
if (confirm('Are you sure you want to delete this record?')) {
|
|
41
45
|
const keyFilter = this.getKeyFilters(record);
|
|
42
|
-
this.backendService.delete(this.apiName, keyFilter).subscribe({
|
|
46
|
+
this.backendService.delete(this.apiName(), keyFilter).subscribe({
|
|
43
47
|
next: () => this.fetchRecords(),
|
|
44
48
|
error: (err) => console.error('Delete failed', err),
|
|
45
49
|
});
|
|
@@ -47,16 +51,9 @@ export class TableList extends BaseView implements OnInit {
|
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
protected handleUpdate(result: {[key: string]: any}, isNew: boolean, _original: {[key: string]: any}): void {
|
|
50
|
-
if (isNew
|
|
51
|
-
alert('Missing authorization to create records');
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
if (!isNew && !this.canUpdate()) {
|
|
55
|
-
alert('Missing authorization to update records');
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
54
|
+
if (!this.requireAuth(() => isNew ? this.canCreate() : this.canUpdate(), isNew ? 'create' : 'update')) return;
|
|
58
55
|
if (confirm('Are you sure you want to save this record?')) {
|
|
59
|
-
this.backendService.post(this.apiName, result).subscribe({
|
|
56
|
+
this.backendService.post(this.apiName(), result).subscribe({
|
|
60
57
|
next: () => this.fetchRecords(),
|
|
61
58
|
error: (err) => console.error('Save failed', err),
|
|
62
59
|
});
|
|
@@ -64,11 +61,8 @@ export class TableList extends BaseView implements OnInit {
|
|
|
64
61
|
}
|
|
65
62
|
|
|
66
63
|
fetchRecords() {
|
|
67
|
-
if (!this.canRead())
|
|
68
|
-
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
this.backendService.list<{[key: string]: any}>(this.apiName, this.searchTerms).subscribe({
|
|
64
|
+
if (!this.requireAuth(() => this.canRead(), 'read')) return;
|
|
65
|
+
this.backendService.list<{[key: string]: any}>(this.apiName(), this.searchTerms).subscribe({
|
|
72
66
|
next: (records) => {
|
|
73
67
|
this.records = records;
|
|
74
68
|
this.updateDisplayedColumns(records);
|
|
@@ -78,27 +72,9 @@ export class TableList extends BaseView implements OnInit {
|
|
|
78
72
|
});
|
|
79
73
|
}
|
|
80
74
|
|
|
81
|
-
/**
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
* For table-level actions, `record` is undefined and the body is empty.
|
|
85
|
-
* Refreshes the row list on success — most actions mutate visible
|
|
86
|
-
* state (set_default flipping defaults, etc.).
|
|
87
|
-
*/
|
|
88
|
-
executeAction(action: TableAction, record?: {[key: string]: unknown}): void {
|
|
89
|
-
if (!this.canExecuteAction(action)) {
|
|
90
|
-
alert(`Missing authorization for action ${action.authorityObject}/${action.authorityCheck}`);
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
if (action.confirmMessage && !confirm(action.confirmMessage)) return;
|
|
94
|
-
const body = action.recordSpecific && record ? this.primaryKeyValues(record) : {};
|
|
95
|
-
this.backendService.executeAction(action.method, body).subscribe({
|
|
96
|
-
next: () => this.fetchRecords(),
|
|
97
|
-
error: (err) => {
|
|
98
|
-
console.error(`Action ${action.action} failed`, err);
|
|
99
|
-
alert(err?.error?.detail ?? `Failed to ${action.caption}`);
|
|
100
|
-
},
|
|
101
|
-
});
|
|
75
|
+
/** Refresh the row list on successful action — most actions mutate visible state (set_default flipping defaults, etc.). */
|
|
76
|
+
protected override onActionSuccess(): void {
|
|
77
|
+
this.fetchRecords();
|
|
102
78
|
}
|
|
103
79
|
|
|
104
80
|
handleQueryParams(params: Params) {
|
|
@@ -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
|
+
<sail-record-form [tableName]="tableName()" [record]="searchRecord" [fields]="searchColumns" [isReadOnly]="isReadOnlyBound"></sail-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>
|
|
@@ -4,10 +4,9 @@ import { MAT_DIALOG_DATA, MatDialogActions, MatDialogClose, MatDialogContent, Ma
|
|
|
4
4
|
import { MatIconModule } from "@angular/material/icon";
|
|
5
5
|
import { RecordForm } from "../form/form_record";
|
|
6
6
|
import { BaseView } from "../abstract/base_view";
|
|
7
|
-
import { BackendService } from "../../service/rest_service";
|
|
8
7
|
|
|
9
8
|
@Component({
|
|
10
|
-
selector: 'table-lookup',
|
|
9
|
+
selector: 'sail-table-lookup',
|
|
11
10
|
templateUrl: './table_lookup.html',
|
|
12
11
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
13
12
|
encapsulation: ViewEncapsulation.None,
|
|
@@ -22,19 +21,17 @@ import { BackendService } from "../../service/rest_service";
|
|
|
22
21
|
],
|
|
23
22
|
})
|
|
24
23
|
export class TableLookup extends BaseView implements OnInit {
|
|
25
|
-
override apiName = '';
|
|
26
24
|
searchRecord: any = {};
|
|
27
25
|
searchColumns: string[] = [];
|
|
28
26
|
|
|
29
|
-
private readonly backendService = inject(BackendService);
|
|
30
27
|
private readonly dialogRef = inject(MatDialogRef<TableLookup>);
|
|
31
28
|
private readonly data = inject<{tableName?: string; apiName?: string}>(MAT_DIALOG_DATA);
|
|
32
29
|
protected readonly cdr = inject(ChangeDetectorRef);
|
|
33
30
|
|
|
34
31
|
ngOnInit() {
|
|
35
32
|
if (this.data) {
|
|
36
|
-
this.tableName
|
|
37
|
-
this.apiName
|
|
33
|
+
this.tableName.set(this.data.tableName ?? '');
|
|
34
|
+
this.apiName.set(this.data.apiName ?? '');
|
|
38
35
|
}
|
|
39
36
|
this.searchColumns = this.getDisplayedColumns();
|
|
40
37
|
this.searchRecord = this.emptyRecord();
|
|
@@ -42,13 +39,14 @@ export class TableLookup extends BaseView implements OnInit {
|
|
|
42
39
|
}
|
|
43
40
|
|
|
44
41
|
private resolveApi() {
|
|
45
|
-
|
|
42
|
+
const tableName = this.tableName();
|
|
43
|
+
if (!this.apiName() && tableName) {
|
|
46
44
|
this.cacheService.getAppData().subscribe((data) => {
|
|
47
45
|
if (data.Apis) {
|
|
48
46
|
for (const api in data.Apis) {
|
|
49
47
|
const entry = data.Apis[api];
|
|
50
|
-
if (entry.Table.TableName ===
|
|
51
|
-
this.apiName
|
|
48
|
+
if (entry.Table.TableName === tableName) {
|
|
49
|
+
this.apiName.set(entry.Version ? entry.Version + '/' + api : api);
|
|
52
50
|
break;
|
|
53
51
|
}
|
|
54
52
|
}
|
|
@@ -59,11 +57,12 @@ export class TableLookup extends BaseView implements OnInit {
|
|
|
59
57
|
|
|
60
58
|
onSearch() {
|
|
61
59
|
this.resolveApi();
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
const apiName = this.apiName();
|
|
61
|
+
if (!apiName) {
|
|
62
|
+
console.error('API name not found for table ' + this.tableName());
|
|
64
63
|
return;
|
|
65
64
|
}
|
|
66
|
-
this.backendService.list<{[key: string]: any}>(
|
|
65
|
+
this.backendService.list<{[key: string]: any}>(apiName, this.buildSearchTerms(this.searchRecord)).subscribe({
|
|
67
66
|
next: (records) => {
|
|
68
67
|
this.records = records;
|
|
69
68
|
this.updateDisplayedColumns(records);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ChangeDetectionStrategy, Component, computed, effect, inject, input, signal } from "@angular/core";
|
|
2
|
-
import {
|
|
2
|
+
import { HttpParams } from "@angular/common/http";
|
|
3
3
|
import { FormsModule } from "@angular/forms";
|
|
4
4
|
import { MatCheckboxModule } from "@angular/material/checkbox";
|
|
5
5
|
import { MatFormFieldModule } from "@angular/material/form-field";
|
|
@@ -7,12 +7,13 @@ import { MatInputModule } from "@angular/material/input";
|
|
|
7
7
|
import { MatSelectModule } from "@angular/material/select";
|
|
8
8
|
import { MatButtonModule } from "@angular/material/button";
|
|
9
9
|
import { BaseAuthService } from "../../service/auth.service";
|
|
10
|
-
import {
|
|
10
|
+
import { BaseRestService } from "../../service/base_rest.service";
|
|
11
11
|
import { ConstantValue } from "../../model/common";
|
|
12
12
|
import { ReportParam } from "../../model/appdata";
|
|
13
|
+
import { titleCase } from "../../util/text";
|
|
13
14
|
|
|
14
15
|
@Component({
|
|
15
|
-
selector: 'table-report',
|
|
16
|
+
selector: 'sail-table-report',
|
|
16
17
|
templateUrl: './table_report.html',
|
|
17
18
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
18
19
|
imports: [
|
|
@@ -24,11 +25,10 @@ import { ReportParam } from "../../model/appdata";
|
|
|
24
25
|
MatButtonModule,
|
|
25
26
|
],
|
|
26
27
|
})
|
|
27
|
-
export class TableReport {
|
|
28
|
+
export class TableReport extends BaseRestService {
|
|
28
29
|
readonly apiName = input('');
|
|
29
30
|
readonly tableName = input('');
|
|
30
31
|
|
|
31
|
-
private readonly http = inject(HttpClient);
|
|
32
32
|
private readonly auth = inject(BaseAuthService);
|
|
33
33
|
|
|
34
34
|
readonly records = signal<Record<string, unknown>[]>([]);
|
|
@@ -49,7 +49,7 @@ export class TableReport {
|
|
|
49
49
|
*/
|
|
50
50
|
readonly reportTitle = computed(() => {
|
|
51
51
|
const reportId = this.apiName().split('/').pop() ?? '';
|
|
52
|
-
return this.auth.getReport(reportId)?.Description ||
|
|
52
|
+
return this.auth.getReport(reportId)?.Description || titleCase(this.tableName());
|
|
53
53
|
});
|
|
54
54
|
|
|
55
55
|
/**
|
|
@@ -65,6 +65,7 @@ export class TableReport {
|
|
|
65
65
|
});
|
|
66
66
|
|
|
67
67
|
constructor() {
|
|
68
|
+
super();
|
|
68
69
|
effect(() => {
|
|
69
70
|
const api = this.apiName();
|
|
70
71
|
if (!api) return;
|
|
@@ -133,8 +134,7 @@ export class TableReport {
|
|
|
133
134
|
httpParams = httpParams.set(k, v);
|
|
134
135
|
}
|
|
135
136
|
}
|
|
136
|
-
|
|
137
|
-
this.http.get<Record<string, unknown>[]>(url, { params: httpParams }).subscribe({
|
|
137
|
+
this.http.get<Record<string, unknown>[]>(this.url('/api/' + api), { params: httpParams }).subscribe({
|
|
138
138
|
next: (rows) => {
|
|
139
139
|
this.records.set(rows ?? []);
|
|
140
140
|
if (rows?.length) {
|
|
@@ -145,14 +145,11 @@ export class TableReport {
|
|
|
145
145
|
this.errorMessage.set('');
|
|
146
146
|
},
|
|
147
147
|
error: (err) => {
|
|
148
|
-
this.errorMessage.set(err?.error ?? 'Failed to load report data.');
|
|
148
|
+
this.errorMessage.set(err?.error?.detail ?? err?.error?.message ?? 'Failed to load report data.');
|
|
149
149
|
},
|
|
150
150
|
});
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
.map((word: string) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
156
|
-
.join(' ');
|
|
157
|
-
}
|
|
153
|
+
/** Re-exported for templates; same as the shared util. */
|
|
154
|
+
titleCase(str: string): string { return titleCase(str); }
|
|
158
155
|
}
|