@nauticana/sail 0.5.4 → 0.5.5
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nauticana/sail",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.5",
|
|
4
4
|
"description": "Metadata-driven Angular component library for CRUD admin frontends backed by the keel Go backend. Tables, forms, navigation, OTP/2FA/social auth, billing.",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"files": [
|
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
<h2>{{ reportTitle() }}</h2>
|
|
2
2
|
|
|
3
|
+
@if (params().length > 0) {
|
|
4
|
+
<form class="report-params" (ngSubmit)="runReport()">
|
|
5
|
+
@for (param of params(); track param.Name) {
|
|
6
|
+
@let opts = optionsFor(param);
|
|
7
|
+
@if (opts) {
|
|
8
|
+
<mat-form-field appearance="outline">
|
|
9
|
+
<mat-label>{{ titleCase(param.Name) }}</mat-label>
|
|
10
|
+
<mat-select [value]="paramValue(param.Name)"
|
|
11
|
+
(valueChange)="setParam(param.Name, $event)">
|
|
12
|
+
<!-- Empty option lets the user clear the filter — the empty
|
|
13
|
+
value is dropped before the request is built so the
|
|
14
|
+
backend sees no constraint on this param. -->
|
|
15
|
+
<mat-option [value]="''">Any</mat-option>
|
|
16
|
+
@for (opt of opts; track opt.Value) {
|
|
17
|
+
<mat-option [value]="opt.Value">{{ opt.Caption }}</mat-option>
|
|
18
|
+
}
|
|
19
|
+
</mat-select>
|
|
20
|
+
</mat-form-field>
|
|
21
|
+
} @else {
|
|
22
|
+
<mat-form-field appearance="outline">
|
|
23
|
+
<mat-label>{{ titleCase(param.Name) }}</mat-label>
|
|
24
|
+
<input matInput [type]="inputTypeFor(param)"
|
|
25
|
+
[value]="paramValue(param.Name)"
|
|
26
|
+
(input)="setParam(param.Name, $any($event.target).value)">
|
|
27
|
+
</mat-form-field>
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
<button mat-raised-button color="primary" type="submit">Run</button>
|
|
31
|
+
</form>
|
|
32
|
+
}
|
|
33
|
+
|
|
3
34
|
@if (errorMessage()) {
|
|
4
35
|
<p class="empty-state">{{ errorMessage() }}</p>
|
|
5
36
|
} @else if (records().length === 0) {
|
|
@@ -1,14 +1,28 @@
|
|
|
1
1
|
import { ChangeDetectionStrategy, Component, computed, effect, inject, input, signal } from "@angular/core";
|
|
2
|
-
import { HttpClient } from "@angular/common/http";
|
|
2
|
+
import { HttpClient, HttpParams } from "@angular/common/http";
|
|
3
|
+
import { FormsModule } from "@angular/forms";
|
|
3
4
|
import { MatCheckboxModule } from "@angular/material/checkbox";
|
|
5
|
+
import { MatFormFieldModule } from "@angular/material/form-field";
|
|
6
|
+
import { MatInputModule } from "@angular/material/input";
|
|
7
|
+
import { MatSelectModule } from "@angular/material/select";
|
|
8
|
+
import { MatButtonModule } from "@angular/material/button";
|
|
4
9
|
import { BaseAuthService } from "../../service/auth.service";
|
|
5
10
|
import { RestURL } from "../../service/rest_url";
|
|
11
|
+
import { ConstantValue } from "../../model/common";
|
|
12
|
+
import { ReportParam } from "../../model/appdata";
|
|
6
13
|
|
|
7
14
|
@Component({
|
|
8
15
|
selector: 'table-report',
|
|
9
16
|
templateUrl: './table_report.html',
|
|
10
17
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
11
|
-
imports: [
|
|
18
|
+
imports: [
|
|
19
|
+
FormsModule,
|
|
20
|
+
MatCheckboxModule,
|
|
21
|
+
MatFormFieldModule,
|
|
22
|
+
MatInputModule,
|
|
23
|
+
MatSelectModule,
|
|
24
|
+
MatButtonModule,
|
|
25
|
+
],
|
|
12
26
|
})
|
|
13
27
|
export class TableReport {
|
|
14
28
|
readonly apiName = input('');
|
|
@@ -21,6 +35,13 @@ export class TableReport {
|
|
|
21
35
|
readonly displayedColumns = signal<string[]>([]);
|
|
22
36
|
readonly errorMessage = signal('');
|
|
23
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Per-parameter input bindings, keyed by param name. Empty string when
|
|
40
|
+
* the user hasn't entered a value yet — fetchReport drops empties so
|
|
41
|
+
* the URL doesn't carry `?param=` for unset fields.
|
|
42
|
+
*/
|
|
43
|
+
readonly paramValues = signal<Record<string, string>>({});
|
|
44
|
+
|
|
24
45
|
/**
|
|
25
46
|
* Report header text — long-form description from rest_report_header.
|
|
26
47
|
* Falls back to the title-cased table name only if the lookup misses
|
|
@@ -31,6 +52,18 @@ export class TableReport {
|
|
|
31
52
|
return this.auth.getReport(reportId)?.Description || this.titleCase(this.tableName());
|
|
32
53
|
});
|
|
33
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Param definitions for the current report. Empty when the report has
|
|
57
|
+
* no rest_report_param rows — in that case we fetch immediately. With
|
|
58
|
+
* params we render an input form and wait for the user to click "Run",
|
|
59
|
+
* preserving the standard "filter then fetch" UX for parameterized
|
|
60
|
+
* reports.
|
|
61
|
+
*/
|
|
62
|
+
readonly params = computed<ReportParam[]>(() => {
|
|
63
|
+
const reportId = this.apiName().split('/').pop() ?? '';
|
|
64
|
+
return this.auth.getReport(reportId)?.Params ?? [];
|
|
65
|
+
});
|
|
66
|
+
|
|
34
67
|
constructor() {
|
|
35
68
|
effect(() => {
|
|
36
69
|
const api = this.apiName();
|
|
@@ -40,18 +73,76 @@ export class TableReport {
|
|
|
40
73
|
this.errorMessage.set('You do not have permission to view this report.');
|
|
41
74
|
return;
|
|
42
75
|
}
|
|
43
|
-
|
|
76
|
+
// Auto-fetch only when the report takes no inputs.
|
|
77
|
+
if (this.params().length === 0) {
|
|
78
|
+
this.fetchReport(api, {});
|
|
79
|
+
}
|
|
44
80
|
});
|
|
45
81
|
}
|
|
46
82
|
|
|
47
|
-
|
|
83
|
+
/**
|
|
84
|
+
* Look up the constant_value options for a parameter's domain.
|
|
85
|
+
* Returns undefined when the parameter has no ConstantId — caller
|
|
86
|
+
* renders a plain input in that case.
|
|
87
|
+
*/
|
|
88
|
+
optionsFor(param: ReportParam): ConstantValue[] | undefined {
|
|
89
|
+
if (!param.ConstantId) return undefined;
|
|
90
|
+
return this.auth.getDomainValues(param.ConstantId);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Native HTML input type derived from a parameter's data_type. Used
|
|
95
|
+
* only when the parameter has no constant domain.
|
|
96
|
+
*/
|
|
97
|
+
inputTypeFor(param: ReportParam): string {
|
|
98
|
+
switch ((param.DataType || '').toLowerCase()) {
|
|
99
|
+
case 'int':
|
|
100
|
+
case 'integer':
|
|
101
|
+
case 'number':
|
|
102
|
+
case 'numeric':
|
|
103
|
+
return 'number';
|
|
104
|
+
case 'date':
|
|
105
|
+
return 'date';
|
|
106
|
+
case 'datetime':
|
|
107
|
+
case 'timestamp':
|
|
108
|
+
return 'datetime-local';
|
|
109
|
+
default:
|
|
110
|
+
return 'text';
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
setParam(name: string, value: string) {
|
|
115
|
+
this.paramValues.update((current) => ({ ...current, [name]: value }));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
paramValue(name: string): string {
|
|
119
|
+
return this.paramValues()[name] ?? '';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
runReport() {
|
|
123
|
+
this.fetchReport(this.apiName(), this.paramValues());
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private fetchReport(api: string, params: Record<string, string>) {
|
|
127
|
+
let httpParams = new HttpParams();
|
|
128
|
+
for (const [k, v] of Object.entries(params)) {
|
|
129
|
+
// Drop empty values so the URL doesn't carry `?param=` for
|
|
130
|
+
// unset fields — backends should treat absent and empty-string
|
|
131
|
+
// the same, sending neither is the cleanest signal.
|
|
132
|
+
if (v !== '' && v != null) {
|
|
133
|
+
httpParams = httpParams.set(k, v);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
48
136
|
const url = RestURL.httpHost + '/api/' + api;
|
|
49
|
-
this.http.get<Record<string, unknown>[]>(url).subscribe({
|
|
137
|
+
this.http.get<Record<string, unknown>[]>(url, { params: httpParams }).subscribe({
|
|
50
138
|
next: (rows) => {
|
|
51
139
|
this.records.set(rows ?? []);
|
|
52
140
|
if (rows?.length) {
|
|
53
141
|
this.displayedColumns.set(Object.keys(rows[0]));
|
|
142
|
+
} else {
|
|
143
|
+
this.displayedColumns.set([]);
|
|
54
144
|
}
|
|
145
|
+
this.errorMessage.set('');
|
|
55
146
|
},
|
|
56
147
|
error: (err) => {
|
|
57
148
|
this.errorMessage.set(err?.error ?? 'Failed to load report data.');
|
package/src/model/appdata.ts
CHANGED
|
@@ -101,8 +101,15 @@ export interface DictionaryPath {
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
export interface ReportParam {
|
|
104
|
-
Name:
|
|
105
|
-
DataType:
|
|
104
|
+
Name: string;
|
|
105
|
+
DataType: string;
|
|
106
|
+
/**
|
|
107
|
+
* When set, the parameter input renders as a dropdown populated from
|
|
108
|
+
* the matching constant_value rows (Pass/Fail/etc.) instead of a free-text
|
|
109
|
+
* input. Empty string / undefined means no domain — render a plain input
|
|
110
|
+
* typed by DataType.
|
|
111
|
+
*/
|
|
112
|
+
ConstantId?: string;
|
|
106
113
|
}
|
|
107
114
|
|
|
108
115
|
export interface RestReport {
|