@nauticana/sail 1.1.9 → 1.1.11
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 +4 -2
- package/package.json +5 -1
- package/src/component/abstract/base_async.ts +9 -11
- package/src/component/abstract/base_form.ts +6 -5
- package/src/component/abstract/base_table.ts +131 -58
- package/src/component/abstract/base_view.ts +43 -27
- package/src/component/account/profile_editor.ts +2 -2
- package/src/component/billing/payment_methods.ts +1 -2
- package/src/component/billing/price_selector.html +2 -1
- package/src/component/billing/price_selector.ts +28 -13
- package/src/component/billing/trial_banner.ts +7 -1
- package/src/component/billing/usage_meter.ts +2 -2
- package/src/component/dashboard/data_card.html +1 -1
- package/src/component/dashboard/entity_selector.ts +21 -23
- package/src/component/form/form_field.html +6 -5
- package/src/component/form/form_field.ts +39 -21
- package/src/component/form/form_record.html +3 -3
- package/src/component/form/form_record.ts +23 -8
- package/src/component/form/form_table.html +2 -2
- package/src/component/form/form_table.ts +6 -5
- package/src/component/login/account_deletion.ts +6 -6
- package/src/component/login/chpass_component.html +4 -4
- package/src/component/login/chpass_component.ts +0 -1
- package/src/component/login/confirm_chpass_component.html +4 -4
- package/src/component/login/confirm_chpass_component.ts +1 -4
- package/src/component/login/consent_gate.html +6 -6
- package/src/component/login/consent_gate.ts +17 -24
- package/src/component/login/login_component.html +7 -4
- package/src/component/login/login_component.ts +9 -5
- package/src/component/login/otp_input.html +13 -5
- package/src/component/login/otp_input.ts +34 -27
- package/src/component/login/register_component.html +31 -14
- package/src/component/login/register_component.ts +80 -89
- package/src/component/login/sms_consent.html +11 -0
- package/src/component/login/sms_consent.ts +38 -0
- package/src/component/login/social_login.html +2 -2
- package/src/component/login/social_login.ts +20 -13
- package/src/component/navigation/navigation.html +2 -2
- package/src/component/navigation/navigation.ts +2 -0
- package/src/component/oauth_connection/oauth_connection_panel.ts +24 -3
- package/src/component/payment/user_payment_methods.html +2 -2
- package/src/component/payment/user_payment_methods.ts +12 -23
- package/src/component/payout/bank_info_form.html +4 -4
- package/src/component/payout/bank_info_form.ts +22 -26
- package/src/component/payout/provider_onboarding.html +5 -5
- package/src/component/payout/provider_onboarding.ts +28 -48
- package/src/component/security/trusted_devices.html +1 -1
- package/src/component/security/trusted_devices.ts +0 -1
- package/src/component/security/twofactor_setup.html +8 -4
- package/src/component/security/twofactor_setup.ts +21 -3
- package/src/component/security/twofactor_verify.html +2 -1
- package/src/component/security/twofactor_verify.ts +2 -8
- package/src/component/table/table_detail.html +15 -14
- package/src/component/table/table_detail.ts +62 -41
- package/src/component/table/table_edit.html +15 -11
- package/src/component/table/table_edit.ts +94 -67
- package/src/component/table/table_list.html +45 -12
- package/src/component/table/table_list.ts +98 -26
- package/src/component/table/table_lookup.html +11 -8
- package/src/component/table/table_lookup.ts +45 -37
- package/src/component/table/table_report.html +2 -2
- package/src/component/table/table_report.ts +37 -31
- package/src/component/table/table_search.html +4 -4
- package/src/component/table/table_search.ts +36 -45
- package/src/config.ts +5 -1
- package/src/index.ts +2 -0
- package/src/model/appdata.ts +5 -9
- package/src/model/common.ts +27 -27
- package/src/model/decorator.ts +25 -21
- package/src/service/api-response.interceptor.ts +4 -1
- package/src/service/auth.interceptor.ts +27 -7
- package/src/service/auth.service.ts +188 -134
- package/src/service/label.service.ts +6 -5
- package/src/service/oauth_connection.service.ts +2 -2
- package/src/service/rest_service.ts +57 -13
- package/src/service/rest_url.ts +23 -7
- package/src/service/script_loader.ts +12 -2
- package/src/util/errors.ts +20 -0
- package/src/util/money.ts +19 -7
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { ChangeDetectionStrategy,
|
|
1
|
+
import { ChangeDetectionStrategy, Component, computed, inject, OnInit, signal, ViewEncapsulation } from "@angular/core";
|
|
2
2
|
import { MatButtonModule } from "@angular/material/button";
|
|
3
3
|
import { MAT_DIALOG_DATA, MatDialogActions, MatDialogClose, MatDialogContent, MatDialogRef, MatDialogTitle } from "@angular/material/dialog";
|
|
4
4
|
import { MatIconModule } from "@angular/material/icon";
|
|
5
|
-
import { take } from "rxjs";
|
|
5
|
+
import { map, Observable, of, switchMap, take } from "rxjs";
|
|
6
6
|
import { RecordForm } from "../form/form_record";
|
|
7
|
+
import { errorDetail } from "../../util/errors";
|
|
7
8
|
import { BaseView } from "../abstract/base_view";
|
|
8
9
|
|
|
9
10
|
@Component({
|
|
@@ -22,67 +23,74 @@ import { BaseView } from "../abstract/base_view";
|
|
|
22
23
|
],
|
|
23
24
|
})
|
|
24
25
|
export class TableLookup extends BaseView implements OnInit {
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
override readonly tableName = signal('');
|
|
27
|
+
readonly searchRecord = signal<Record<string, unknown>>({});
|
|
28
|
+
readonly searchColumns = this.metadataColumns;
|
|
27
29
|
|
|
28
30
|
private readonly dialogRef = inject(MatDialogRef<TableLookup>);
|
|
29
31
|
private readonly data = inject<{tableName?: string; apiName?: string}>(MAT_DIALOG_DATA);
|
|
30
|
-
|
|
32
|
+
|
|
33
|
+
/** A lookup only selects — no edit/delete, so no appended "actions" column. */
|
|
34
|
+
protected override includeActionsColumn(): boolean {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
31
37
|
|
|
32
38
|
ngOnInit() {
|
|
33
39
|
if (this.data) {
|
|
34
40
|
this.tableName.set(this.data.tableName ?? '');
|
|
35
41
|
this.apiName.set(this.data.apiName ?? '');
|
|
36
42
|
}
|
|
37
|
-
this.
|
|
38
|
-
this.
|
|
39
|
-
this.resolveApi();
|
|
43
|
+
this.searchRecord.set(this.emptyRecord());
|
|
44
|
+
this.resolveApi().subscribe();
|
|
40
45
|
}
|
|
41
46
|
|
|
42
|
-
|
|
47
|
+
/** Resolve (and cache) the api serving this table; '' when none matches. */
|
|
48
|
+
private resolveApi(): Observable<string> {
|
|
49
|
+
const apiName = this.apiName();
|
|
50
|
+
if (apiName) return of(apiName);
|
|
43
51
|
const tableName = this.tableName();
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
break;
|
|
52
|
-
}
|
|
52
|
+
return this.cacheService.getAppData().pipe(
|
|
53
|
+
take(1),
|
|
54
|
+
map((data) => {
|
|
55
|
+
for (const api in data.Apis ?? {}) {
|
|
56
|
+
if (data.Apis[api].Table?.TableName === tableName) {
|
|
57
|
+
this.apiName.set(api); // bare RestUri; BackendService.crudUrl resolves the version
|
|
58
|
+
return api;
|
|
53
59
|
}
|
|
54
60
|
}
|
|
55
|
-
|
|
56
|
-
|
|
61
|
+
return '';
|
|
62
|
+
}),
|
|
63
|
+
);
|
|
57
64
|
}
|
|
58
65
|
|
|
59
66
|
onSearch() {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
// Chained on resolveApi so an early click waits instead of silently no-oping.
|
|
68
|
+
this.resolveApi().pipe(
|
|
69
|
+
switchMap((apiName) => {
|
|
70
|
+
if (!apiName) {
|
|
71
|
+
this.viewError.set(`No API found for table ${this.tableName()}`);
|
|
72
|
+
return of<Record<string, unknown>[]>([]);
|
|
73
|
+
}
|
|
74
|
+
return this.backendService.list<Record<string, unknown>>(apiName, this.buildSearchTerms(this.searchRecord()));
|
|
75
|
+
}),
|
|
76
|
+
).subscribe({
|
|
67
77
|
next: (records) => {
|
|
68
|
-
this.records
|
|
78
|
+
this.records.set(records);
|
|
69
79
|
this.updateDisplayedColumns(records);
|
|
70
|
-
this.cdr.markForCheck();
|
|
71
80
|
},
|
|
72
|
-
error: (err) =>
|
|
81
|
+
error: (err) => {
|
|
82
|
+
console.error('Lookup search failed', err);
|
|
83
|
+
this.viewError.set(errorDetail(err, 'Search failed.'));
|
|
84
|
+
},
|
|
73
85
|
});
|
|
74
86
|
}
|
|
75
87
|
|
|
76
|
-
onSelect(record:
|
|
88
|
+
onSelect(record: Record<string, unknown>) {
|
|
77
89
|
this.dialogRef.close(record);
|
|
78
90
|
}
|
|
79
91
|
|
|
80
92
|
onClear() {
|
|
81
|
-
this.searchRecord
|
|
82
|
-
this.records
|
|
93
|
+
this.searchRecord.set(this.emptyRecord());
|
|
94
|
+
this.records.set([]);
|
|
83
95
|
}
|
|
84
|
-
|
|
85
|
-
protected handleDelete(_record: {[key: string]: any}): void {}
|
|
86
|
-
|
|
87
|
-
protected handleUpdate(_result: {[key: string]: any}, _isNew: boolean, _original: {[key: string]: any}): void {}
|
|
88
96
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<h2>{{ reportTitle() }}</h2>
|
|
2
2
|
|
|
3
3
|
@if (params().length > 0) {
|
|
4
|
-
<form class="report-params" (
|
|
4
|
+
<form class="report-params" (submit)="onSubmit($event)">
|
|
5
5
|
@for (param of params(); track param.Name) {
|
|
6
6
|
@let opts = optionsFor(param);
|
|
7
7
|
@if (opts) {
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
<mat-label>{{ titleCase(param.Name) }}</mat-label>
|
|
24
24
|
<input matInput [type]="inputTypeFor(param)"
|
|
25
25
|
[value]="paramValue(param.Name)"
|
|
26
|
-
(input)="
|
|
26
|
+
(input)="onParamInput(param.Name, $event)">
|
|
27
27
|
</mat-form-field>
|
|
28
28
|
}
|
|
29
29
|
}
|
|
@@ -1,23 +1,21 @@
|
|
|
1
|
-
import { ChangeDetectionStrategy, Component, computed, effect, inject, input, signal } from "@angular/core";
|
|
2
|
-
import { HttpParams } from "@angular/common/http";
|
|
3
|
-
import { FormsModule } from "@angular/forms";
|
|
1
|
+
import { ChangeDetectionStrategy, Component, computed, effect, inject, input, signal, untracked } from "@angular/core";
|
|
4
2
|
import { MatCheckboxModule } from "@angular/material/checkbox";
|
|
5
3
|
import { MatFormFieldModule } from "@angular/material/form-field";
|
|
6
4
|
import { MatInputModule } from "@angular/material/input";
|
|
7
5
|
import { MatSelectModule } from "@angular/material/select";
|
|
8
6
|
import { MatButtonModule } from "@angular/material/button";
|
|
9
7
|
import { BaseAuthService } from "../../service/auth.service";
|
|
10
|
-
import {
|
|
8
|
+
import { BackendService } from "../../service/rest_service";
|
|
11
9
|
import { ConstantValue } from "../../model/common";
|
|
12
10
|
import { ReportParam } from "../../model/appdata";
|
|
13
11
|
import { titleCase } from "../../util/text";
|
|
12
|
+
import { errorDetail } from "../../util/errors";
|
|
14
13
|
|
|
15
14
|
@Component({
|
|
16
15
|
selector: 'sail-table-report',
|
|
17
16
|
templateUrl: './table_report.html',
|
|
18
17
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
19
18
|
imports: [
|
|
20
|
-
FormsModule,
|
|
21
19
|
MatCheckboxModule,
|
|
22
20
|
MatFormFieldModule,
|
|
23
21
|
MatInputModule,
|
|
@@ -25,11 +23,12 @@ import { titleCase } from "../../util/text";
|
|
|
25
23
|
MatButtonModule,
|
|
26
24
|
],
|
|
27
25
|
})
|
|
28
|
-
export class TableReport
|
|
26
|
+
export class TableReport {
|
|
29
27
|
readonly apiName = input('');
|
|
30
28
|
readonly tableName = input('');
|
|
31
29
|
|
|
32
30
|
private readonly auth = inject(BaseAuthService);
|
|
31
|
+
private readonly backend = inject(BackendService);
|
|
33
32
|
|
|
34
33
|
readonly records = signal<Record<string, unknown>[]>([]);
|
|
35
34
|
readonly displayedColumns = signal<string[]>([]);
|
|
@@ -50,11 +49,7 @@ export class TableReport extends BaseRestService {
|
|
|
50
49
|
*/
|
|
51
50
|
readonly reportId = computed(() => this.apiName().split('/').pop() ?? '');
|
|
52
51
|
|
|
53
|
-
/**
|
|
54
|
-
* Report header text — long-form description from rest_report_header.
|
|
55
|
-
* Falls back to the title-cased table name only if the lookup misses
|
|
56
|
-
* (e.g. a brand-new report whose row hasn't been seeded yet).
|
|
57
|
-
*/
|
|
52
|
+
/** Long-form header from rest_report_header; falls back to the title-cased table name. */
|
|
58
53
|
readonly reportTitle = computed(() => {
|
|
59
54
|
return this.auth.getReport(this.reportId())?.Description || titleCase(this.tableName());
|
|
60
55
|
});
|
|
@@ -62,26 +57,36 @@ export class TableReport extends BaseRestService {
|
|
|
62
57
|
/**
|
|
63
58
|
* Param definitions for the current report. Empty when the report has
|
|
64
59
|
* no rest_report_param rows — in that case we fetch immediately. With
|
|
65
|
-
* params we render an input form and wait for the user to click "Run"
|
|
66
|
-
* preserving the standard "filter then fetch" UX for parameterized
|
|
67
|
-
* reports.
|
|
60
|
+
* params we render an input form and wait for the user to click "Run".
|
|
68
61
|
*/
|
|
69
62
|
readonly params = computed<ReportParam[]>(() => {
|
|
70
63
|
return this.auth.getReport(this.reportId())?.Params ?? [];
|
|
71
64
|
});
|
|
72
65
|
|
|
66
|
+
private lastApi: string | null = null;
|
|
67
|
+
|
|
73
68
|
constructor() {
|
|
74
|
-
|
|
69
|
+
// Re-runs on apiName changes AND appdata (re)loads via params()/canReport;
|
|
70
|
+
// report state is only reset when the report actually changed, so an
|
|
71
|
+
// appdata refresh never wipes half-typed parameters.
|
|
75
72
|
effect(() => {
|
|
76
73
|
const api = this.apiName();
|
|
77
74
|
if (!api) return;
|
|
75
|
+
const apiChanged = api !== this.lastApi;
|
|
76
|
+
this.lastApi = api;
|
|
77
|
+
if (apiChanged) {
|
|
78
|
+
this.records.set([]);
|
|
79
|
+
this.displayedColumns.set([]);
|
|
80
|
+
this.paramValues.set({});
|
|
81
|
+
}
|
|
82
|
+
this.errorMessage.set('');
|
|
78
83
|
|
|
79
84
|
if (!this.auth.canReport(this.reportId())) {
|
|
80
85
|
this.errorMessage.set('You do not have permission to view this report.');
|
|
81
86
|
return;
|
|
82
87
|
}
|
|
83
|
-
// Auto-fetch only when the report takes no inputs.
|
|
84
|
-
if (this.params().length === 0) {
|
|
88
|
+
// Auto-fetch only when the report takes no inputs and nothing is shown yet.
|
|
89
|
+
if (this.params().length === 0 && (apiChanged || untracked(this.records).length === 0)) {
|
|
85
90
|
this.fetchReport(api, {});
|
|
86
91
|
}
|
|
87
92
|
});
|
|
@@ -122,36 +127,37 @@ export class TableReport extends BaseRestService {
|
|
|
122
127
|
this.paramValues.update((current) => ({ ...current, [name]: value }));
|
|
123
128
|
}
|
|
124
129
|
|
|
130
|
+
onParamInput(name: string, event: Event) {
|
|
131
|
+
this.setParam(name, (event.target as HTMLInputElement).value);
|
|
132
|
+
}
|
|
133
|
+
|
|
125
134
|
paramValue(name: string): string {
|
|
126
135
|
return this.paramValues()[name] ?? '';
|
|
127
136
|
}
|
|
128
137
|
|
|
138
|
+
onSubmit(event: Event) {
|
|
139
|
+
event.preventDefault();
|
|
140
|
+
this.runReport();
|
|
141
|
+
}
|
|
142
|
+
|
|
129
143
|
runReport() {
|
|
130
144
|
this.fetchReport(this.apiName(), this.paramValues());
|
|
131
145
|
}
|
|
132
146
|
|
|
133
147
|
private fetchReport(api: string, params: Record<string, string>) {
|
|
134
|
-
|
|
148
|
+
const filled: Record<string, string> = {};
|
|
135
149
|
for (const [k, v] of Object.entries(params)) {
|
|
136
|
-
// Drop empty values so the URL doesn't carry `?param=` for
|
|
137
|
-
|
|
138
|
-
// the same, sending neither is the cleanest signal.
|
|
139
|
-
if (v !== '' && v != null) {
|
|
140
|
-
httpParams = httpParams.set(k, v);
|
|
141
|
-
}
|
|
150
|
+
// Drop empty values so the URL doesn't carry `?param=` for unset fields.
|
|
151
|
+
if (v !== '' && v != null) filled[k] = v;
|
|
142
152
|
}
|
|
143
|
-
this.
|
|
153
|
+
this.backend.report(api, filled).subscribe({
|
|
144
154
|
next: (rows) => {
|
|
145
155
|
this.records.set(rows ?? []);
|
|
146
|
-
|
|
147
|
-
this.displayedColumns.set(Object.keys(rows[0]));
|
|
148
|
-
} else {
|
|
149
|
-
this.displayedColumns.set([]);
|
|
150
|
-
}
|
|
156
|
+
this.displayedColumns.set(rows?.length ? Object.keys(rows[0]) : []);
|
|
151
157
|
this.errorMessage.set('');
|
|
152
158
|
},
|
|
153
159
|
error: (err) => {
|
|
154
|
-
this.errorMessage.set(err
|
|
160
|
+
this.errorMessage.set(errorDetail(err, 'Failed to load report data.'));
|
|
155
161
|
},
|
|
156
162
|
});
|
|
157
163
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
<h1>
|
|
2
|
-
@if (title) {
|
|
3
|
-
{{ title }}
|
|
2
|
+
@if (title()) {
|
|
3
|
+
{{ title() }}
|
|
4
4
|
} @else {
|
|
5
5
|
{{ getCaption() }} Search
|
|
6
6
|
}
|
|
7
7
|
</h1>
|
|
8
8
|
<div class="page-container">
|
|
9
|
-
<sail-record-form [tableName]="tableName()" [record]="editableRecord" [fields]="searchColumns" [isReadOnly]="
|
|
9
|
+
<sail-record-form [tableName]="tableName()" [record]="editableRecord()" (recordChange)="editableRecord.set($event)" [fields]="searchColumns()" [isReadOnly]="isReadOnlyFn"></sail-record-form>
|
|
10
10
|
</div>
|
|
11
11
|
<div class="search-actions">
|
|
12
12
|
<button matButton="elevated" class="primary" (click)="onSearch()">Search</button>
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
@if (canCreate()) {
|
|
15
15
|
<button matButton="elevated" class="accent" (click)="onAddRecord()">New Record</button>
|
|
16
16
|
}
|
|
17
|
-
@for (action of
|
|
17
|
+
@for (action of tableActions(); track action.action) {
|
|
18
18
|
@if (canExecuteAction(action)) {
|
|
19
19
|
<button matButton="outlined" type="button"
|
|
20
20
|
[title]="action.caption"
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { ChangeDetectionStrategy,
|
|
1
|
+
import { ChangeDetectionStrategy, Component, computed, DestroyRef, inject, input, linkedSignal, OnInit, ViewEncapsulation } from "@angular/core";
|
|
2
|
+
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
|
2
3
|
import { MatButtonModule } from "@angular/material/button";
|
|
3
4
|
import { MatIconModule } from "@angular/material/icon";
|
|
4
5
|
import { ActivatedRoute, Router } from "@angular/router";
|
|
6
|
+
import { take } from "rxjs";
|
|
5
7
|
import { BaseForm } from "../abstract/base_form";
|
|
6
8
|
import { RecordForm } from "../form/form_record";
|
|
7
9
|
import { ApplicationMenu } from "../../model/common";
|
|
@@ -26,11 +28,11 @@ export class TableSearch extends BaseForm implements OnInit {
|
|
|
26
28
|
readonly apiName = linkedSignal<string, string>({ source: () => this.apiNameInput(), computation: (v, p) => v || p?.value || '' });
|
|
27
29
|
readonly targetRoute = linkedSignal<string, string>({ source: () => this.targetRouteInput(), computation: (v, p) => v || p?.value || '' });
|
|
28
30
|
|
|
29
|
-
searchColumns
|
|
31
|
+
readonly searchColumns = this.metadataColumns;
|
|
30
32
|
|
|
31
33
|
private readonly router = inject(Router);
|
|
32
34
|
private readonly route = inject(ActivatedRoute);
|
|
33
|
-
private readonly
|
|
35
|
+
private readonly destroyRef = inject(DestroyRef);
|
|
34
36
|
|
|
35
37
|
/**
|
|
36
38
|
* Search screen surfaces only table-level actions; record-specific actions
|
|
@@ -43,30 +45,31 @@ export class TableSearch extends BaseForm implements OnInit {
|
|
|
43
45
|
if (!this.tableName() && data['tableName']) this.tableName.set(data['tableName']);
|
|
44
46
|
if (!this.apiName() && data['apiName']) this.apiName.set(data['apiName']);
|
|
45
47
|
if (!this.targetRoute() && data['targetRoute']) this.targetRoute.set(data['targetRoute']);
|
|
46
|
-
this.title
|
|
48
|
+
this.title.set(this.getCaption() + ' Search');
|
|
47
49
|
if (!this.targetRoute() && (this.apiName() || this.tableName())) {
|
|
48
|
-
this.cacheService.getMenus().
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
50
|
+
this.cacheService.getMenus().pipe(
|
|
51
|
+
take(1),
|
|
52
|
+
takeUntilDestroyed(this.destroyRef),
|
|
53
|
+
).subscribe((menus: ApplicationMenu[]) => this.resolveTargetRoute(menus));
|
|
54
|
+
}
|
|
55
|
+
this.editableRecord.set(this.emptySearchRecord());
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Exact RestUri match only — substring matching routed `user` to `user_payment_method`. */
|
|
59
|
+
private resolveTargetRoute(menus: ApplicationMenu[]) {
|
|
60
|
+
const apiName = this.apiName();
|
|
61
|
+
const tableName = this.tableName();
|
|
62
|
+
for (const menu of menus) {
|
|
63
|
+
if (!menu.Id) continue;
|
|
64
|
+
for (const page of menu.ApplicationMenuItems ?? []) {
|
|
65
|
+
if (!page.ItemId || !page.RestUri) continue;
|
|
66
|
+
if (page.RestUri === apiName || (!apiName && tableName && page.RestUri === tableName)) {
|
|
67
|
+
const base = '/' + menu.Id.toLowerCase() + '/' + page.ItemId;
|
|
68
|
+
this.targetRoute.set(page.FilterOnList ? base : base + '/list');
|
|
69
|
+
return;
|
|
65
70
|
}
|
|
66
|
-
}
|
|
71
|
+
}
|
|
67
72
|
}
|
|
68
|
-
this.editableRecord = this.emptySearchRecord();
|
|
69
|
-
this.searchColumns = this.getDisplayedColumns();
|
|
70
73
|
}
|
|
71
74
|
|
|
72
75
|
onSearch() {
|
|
@@ -75,35 +78,23 @@ export class TableSearch extends BaseForm implements OnInit {
|
|
|
75
78
|
alert('Navigation target not found for this table');
|
|
76
79
|
return;
|
|
77
80
|
}
|
|
78
|
-
this.router.navigate([targetRoute], {queryParams: this.buildSearchTerms(this.editableRecord)});
|
|
81
|
+
this.router.navigate([targetRoute], {queryParams: this.buildSearchTerms(this.editableRecord())});
|
|
79
82
|
}
|
|
80
83
|
|
|
81
84
|
onClear() {
|
|
82
|
-
this.editableRecord
|
|
85
|
+
this.editableRecord.set(this.emptySearchRecord());
|
|
83
86
|
}
|
|
84
87
|
|
|
85
88
|
/**
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
* default would otherwise have the form submit `?IsPrimary=false` and
|
|
90
|
-
* silently exclude every primary-flag row).
|
|
91
|
-
*
|
|
92
|
-
* The user opts into a boolean filter by clicking the checkbox: first
|
|
93
|
-
* click sets it to `true`, a second click sets it to `false`, and the
|
|
94
|
-
* Clear button resets back to `null` (no filter). `buildSearchTerms`
|
|
95
|
-
* already skips `null` / `undefined` values, so the URL only carries
|
|
96
|
-
* filters the user explicitly toggled.
|
|
89
|
+
* emptyRecord() with every seeded default blanked (booleans to null), so an
|
|
90
|
+
* unmodified Search never ships implicit filters like `?Status=A`.
|
|
91
|
+
* buildSearchTerms skips null/'' — only user-set values reach the URL.
|
|
97
92
|
*/
|
|
98
|
-
private emptySearchRecord():
|
|
93
|
+
private emptySearchRecord(): Record<string, unknown> {
|
|
99
94
|
const rec = this.emptyRecord();
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (col.DataType === 'boolean') {
|
|
104
|
-
rec[col.PascalName] = null;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
95
|
+
for (const key of Object.keys(rec)) {
|
|
96
|
+
if (key === this.config.opField || Array.isArray(rec[key])) continue;
|
|
97
|
+
rec[key] = this.isBoolean(key) ? null : '';
|
|
107
98
|
}
|
|
108
99
|
return rec;
|
|
109
100
|
}
|
package/src/config.ts
CHANGED
|
@@ -31,7 +31,7 @@ export interface SailGuiConfig {
|
|
|
31
31
|
restUrls?: { [key: string]: string };
|
|
32
32
|
publicRoutes?: Routes;
|
|
33
33
|
extraRoutes?: (data: ApplicationData) => Routes;
|
|
34
|
-
dashboardComponent?: Type<
|
|
34
|
+
dashboardComponent?: Type<unknown>;
|
|
35
35
|
appTitle?: string;
|
|
36
36
|
/** Public asset URL for the application logo shown in the navigation home link, e.g. 'logo.png'. */
|
|
37
37
|
appLogoUrl?: string;
|
|
@@ -55,6 +55,10 @@ export interface SailGuiConfig {
|
|
|
55
55
|
appleRedirectUri?: string;
|
|
56
56
|
/** Public URL of your privacy policy. Shown as a link in ConsentGateComponent. */
|
|
57
57
|
privacyPolicyUrl?: string;
|
|
58
|
+
/** Public URL of your terms of service. Shown in the SMS-consent disclosure. */
|
|
59
|
+
termsUrl?: string;
|
|
60
|
+
/** Brand name shown in the SMS/10DLC consent disclosure (e.g. "Daxoom"). */
|
|
61
|
+
appName?: string;
|
|
58
62
|
/** Content hash / version of the currently-deployed privacy policy. */
|
|
59
63
|
defaultPolicyVersion?: string;
|
|
60
64
|
/** ISO 639-1 language code of the policy variant shown ('en', 'fr', ...). */
|
package/src/index.ts
CHANGED
|
@@ -32,6 +32,7 @@ export * from './component/login/chpass_component';
|
|
|
32
32
|
export * from './component/login/confirm_register_component';
|
|
33
33
|
export * from './component/login/confirm_chpass_component';
|
|
34
34
|
export * from './component/login/consent_gate';
|
|
35
|
+
export * from './component/login/sms_consent';
|
|
35
36
|
export * from './component/login/otp_input';
|
|
36
37
|
export * from './component/login/social_login';
|
|
37
38
|
export * from './component/login/account_deletion';
|
|
@@ -69,3 +70,4 @@ export * from './service/label.service';
|
|
|
69
70
|
export * from './service/base_rest.service';
|
|
70
71
|
export * from './util/text';
|
|
71
72
|
export * from './util/money';
|
|
73
|
+
export * from './util/errors';
|
package/src/model/appdata.ts
CHANGED
|
@@ -18,13 +18,6 @@ export interface LoginResponse {
|
|
|
18
18
|
token: string;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
export interface AuthSummary {
|
|
22
|
-
Obj: string;
|
|
23
|
-
Act: string;
|
|
24
|
-
Val: string;
|
|
25
|
-
Reg: string;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
21
|
export interface ApplicationData {
|
|
29
22
|
MainMenu: ApplicationMenu[];
|
|
30
23
|
Permissions: AuthorizationRolePermission[];
|
|
@@ -32,6 +25,8 @@ export interface ApplicationData {
|
|
|
32
25
|
TableCache: {[key: string]: {[key: string]: string}};
|
|
33
26
|
TableDefinitions: {[key: string]: TableDefinition};
|
|
34
27
|
Apis: {[key: string]: DictionaryPath};
|
|
28
|
+
/** keel emits Reports as map[string]*RestReport — a JSON object keyed by report id. */
|
|
29
|
+
Reports?: {[key: string]: RestReport};
|
|
35
30
|
[key: string]: unknown;
|
|
36
31
|
}
|
|
37
32
|
|
|
@@ -195,8 +190,9 @@ export interface PartnerRegistration {
|
|
|
195
190
|
Zipcode: string;
|
|
196
191
|
Country: string;
|
|
197
192
|
Phone: string;
|
|
198
|
-
|
|
199
|
-
|
|
193
|
+
// null when geocoding is disabled or the address wasn't verified.
|
|
194
|
+
Latitude: number | null;
|
|
195
|
+
Longitude: number | null;
|
|
200
196
|
DomainURL: string;
|
|
201
197
|
PlanID: string;
|
|
202
198
|
}
|
package/src/model/common.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { IsSailString, IsSailNumeric } from './decorator';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* SIUD operation codes that travel on every row through keel's generic CRUD.
|
|
@@ -43,32 +43,32 @@ export class SiudAction {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
export class UserAccountPolicy extends SiudAction {
|
|
46
|
-
@
|
|
46
|
+
@IsSailString(30)
|
|
47
47
|
Id?: string;
|
|
48
48
|
|
|
49
|
-
@
|
|
49
|
+
@IsSailNumeric(9, 0)
|
|
50
50
|
PolicyValue?: number;
|
|
51
51
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
|
|
55
55
|
export class ApplicationMenuItem extends SiudAction {
|
|
56
|
-
@
|
|
56
|
+
@IsSailString(30)
|
|
57
57
|
MenuId?: string;
|
|
58
58
|
|
|
59
|
-
@
|
|
59
|
+
@IsSailString(30)
|
|
60
60
|
ItemId?: string;
|
|
61
61
|
|
|
62
|
-
@
|
|
62
|
+
@IsSailString(80)
|
|
63
63
|
Caption?: string;
|
|
64
64
|
|
|
65
|
-
@
|
|
65
|
+
@IsSailNumeric(9, 0)
|
|
66
66
|
DisplayOrder?: number;
|
|
67
67
|
|
|
68
|
-
@
|
|
68
|
+
@IsSailString(255)
|
|
69
69
|
RestUri?: string;
|
|
70
70
|
|
|
71
|
-
@
|
|
71
|
+
@IsSailString(80)
|
|
72
72
|
Icon?: string;
|
|
73
73
|
|
|
74
74
|
IsActive?: boolean;
|
|
@@ -79,16 +79,16 @@ export class ApplicationMenuItem extends SiudAction {
|
|
|
79
79
|
|
|
80
80
|
|
|
81
81
|
export class ApplicationMenu extends SiudAction {
|
|
82
|
-
@
|
|
82
|
+
@IsSailString(30)
|
|
83
83
|
Id?: string;
|
|
84
84
|
|
|
85
|
-
@
|
|
85
|
+
@IsSailString(80)
|
|
86
86
|
Caption?: string;
|
|
87
87
|
|
|
88
|
-
@
|
|
88
|
+
@IsSailNumeric(9, 0)
|
|
89
89
|
DisplayOrder?: number;
|
|
90
90
|
|
|
91
|
-
@
|
|
91
|
+
@IsSailString(80)
|
|
92
92
|
Icon?: string;
|
|
93
93
|
|
|
94
94
|
IsActive?: boolean;
|
|
@@ -98,19 +98,19 @@ export class ApplicationMenu extends SiudAction {
|
|
|
98
98
|
|
|
99
99
|
|
|
100
100
|
export class AuthorizationRolePermission extends SiudAction {
|
|
101
|
-
@
|
|
101
|
+
@IsSailString(30)
|
|
102
102
|
RoleId?: string;
|
|
103
103
|
|
|
104
|
-
@
|
|
104
|
+
@IsSailString(30)
|
|
105
105
|
AuthorizationObjectId?: string;
|
|
106
106
|
|
|
107
|
-
@
|
|
107
|
+
@IsSailString(30)
|
|
108
108
|
Action?: string;
|
|
109
109
|
|
|
110
|
-
@
|
|
110
|
+
@IsSailString(80)
|
|
111
111
|
LowLimit?: string;
|
|
112
112
|
|
|
113
|
-
@
|
|
113
|
+
@IsSailString(80)
|
|
114
114
|
HighLimit?: string;
|
|
115
115
|
|
|
116
116
|
IsActive?: boolean;
|
|
@@ -119,35 +119,35 @@ export class AuthorizationRolePermission extends SiudAction {
|
|
|
119
119
|
|
|
120
120
|
|
|
121
121
|
export class ConstantValue extends SiudAction {
|
|
122
|
-
@
|
|
122
|
+
@IsSailString(60)
|
|
123
123
|
ConstantId?: string;
|
|
124
124
|
|
|
125
|
-
@
|
|
125
|
+
@IsSailString(80)
|
|
126
126
|
Value?: string;
|
|
127
127
|
|
|
128
|
-
@
|
|
128
|
+
@IsSailString(80)
|
|
129
129
|
Caption?: string;
|
|
130
130
|
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
|
|
134
134
|
export class UserAccount extends SiudAction {
|
|
135
|
-
@
|
|
135
|
+
@IsSailNumeric(9, 0)
|
|
136
136
|
Id?: number;
|
|
137
137
|
|
|
138
|
-
@
|
|
138
|
+
@IsSailString(80)
|
|
139
139
|
FirstName?: string;
|
|
140
140
|
|
|
141
|
-
@
|
|
141
|
+
@IsSailString(80)
|
|
142
142
|
LastName?: string;
|
|
143
143
|
|
|
144
|
-
@
|
|
144
|
+
@IsSailString(80)
|
|
145
145
|
UserName?: string;
|
|
146
146
|
|
|
147
|
-
@
|
|
147
|
+
@IsSailString(255)
|
|
148
148
|
UserEmail?: string;
|
|
149
149
|
|
|
150
|
-
@
|
|
150
|
+
@IsSailString(1)
|
|
151
151
|
Status?: string;
|
|
152
152
|
|
|
153
153
|
}
|