@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,59 +1,69 @@
|
|
|
1
|
-
import { inject } from "@angular/core";
|
|
1
|
+
import { inject, signal, WritableSignal } from "@angular/core";
|
|
2
2
|
import { ForeignKey, TableAction, TableColumn, TableDefinition } from "../../model/appdata";
|
|
3
|
-
import { ConstantValue } from "../../model/common";
|
|
3
|
+
import { ConstantValue, LookupStyle, OpCode } from "../../model/common";
|
|
4
4
|
import { BaseAuthService } from "../../service/auth.service";
|
|
5
|
+
import { BackendService } from "../../service/rest_service";
|
|
5
6
|
import { SAIL_GUI_CONFIG, SailGuiConfig, DEFAULT_CONFIG } from "../../config";
|
|
7
|
+
import { titleCase } from "../../util/text";
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
/** CRUD verb used by `requireAuth()` to compose the "Missing authorization" alert. */
|
|
11
|
+
export type AuthVerb = 'create' | 'read' | 'update' | 'delete' | 'view';
|
|
6
12
|
|
|
7
13
|
|
|
8
14
|
export abstract class BaseTable {
|
|
9
15
|
protected readonly cacheService = inject(BaseAuthService);
|
|
16
|
+
protected readonly backendService = inject(BackendService);
|
|
10
17
|
protected readonly config: SailGuiConfig = inject(SAIL_GUI_CONFIG, {optional: true}) ?? DEFAULT_CONFIG;
|
|
11
18
|
protected readonly dialogWidth: string = '400px';
|
|
12
|
-
tableName = '';
|
|
19
|
+
readonly tableName: WritableSignal<string> = signal('');
|
|
20
|
+
/** Optional consumer-set override; when empty the title-cased table name is used. */
|
|
13
21
|
caption = '';
|
|
22
|
+
/** Consumer-set per-field option overrides (per fieldName). When unset, options come from the column's LookupDomain / LookupTable. */
|
|
14
23
|
fieldOptions: {[key: string]: ConstantValue[]} = {};
|
|
15
|
-
fieldLabels: {[key: string]: string} = {};
|
|
16
24
|
isReadOnlyBound = this.isReadOnly.bind(this);
|
|
17
25
|
|
|
18
26
|
isReadOnly(_fieldName: string): boolean {
|
|
19
27
|
return false;
|
|
20
28
|
}
|
|
21
29
|
|
|
22
|
-
titleCase(
|
|
23
|
-
|
|
24
|
-
return word.charAt(0).toUpperCase() + word.slice(1);
|
|
25
|
-
}).join(' ');
|
|
26
|
-
}
|
|
30
|
+
/** Re-exported on the class so templates can call `titleCase(x)` without an extra import in the consumer. */
|
|
31
|
+
titleCase(str: string): string { return titleCase(str); }
|
|
27
32
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
return this.caption;
|
|
33
|
+
/** Display title for the active table; recomputes when `tableName` changes. */
|
|
34
|
+
getCaption(): string {
|
|
35
|
+
return this.caption || titleCase(this.tableName());
|
|
33
36
|
}
|
|
34
37
|
|
|
38
|
+
/** Display label for a column; uses the metadata Caption, otherwise title-cases the field name. */
|
|
35
39
|
colCaption(fieldName: string): string {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
this.fieldLabels[fieldName] = col?.Caption || this.titleCase(fieldName);
|
|
39
|
-
}
|
|
40
|
-
return this.fieldLabels[fieldName];
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
canRead() {
|
|
44
|
-
return this.cacheService.canRead(this.tableName);
|
|
40
|
+
const col = this.getColumn(fieldName);
|
|
41
|
+
return col?.Caption || titleCase(fieldName);
|
|
45
42
|
}
|
|
46
43
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
44
|
+
canRead() { return this.cacheService.canRead(this.tableName()); }
|
|
45
|
+
canCreate() { return this.cacheService.canCreate(this.tableName()); }
|
|
46
|
+
canUpdate() { return this.cacheService.canUpdate(this.tableName()); }
|
|
47
|
+
canDelete() { return this.cacheService.canDelete(this.tableName()); }
|
|
50
48
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
/** True when the record is a locally-inserted row not yet persisted (op_code === 'I'). */
|
|
50
|
+
isInserted(record: Record<string, unknown>): boolean { return record[this.config.opField] === OpCode.Insert; }
|
|
51
|
+
/** True when the record is marked for update (op_code === 'U'). */
|
|
52
|
+
isUpdated(record: Record<string, unknown>): boolean { return record[this.config.opField] === OpCode.Update; }
|
|
53
|
+
/** True when the record is marked for delete (op_code === 'D'). */
|
|
54
|
+
isDeleted(record: Record<string, unknown>): boolean { return record[this.config.opField] === OpCode.Delete; }
|
|
54
55
|
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Authorization guard with shared "Missing authorization" copy. Pass the
|
|
58
|
+
* permission predicate (typically a `canX()` method on this class) and
|
|
59
|
+
* the verb that fills the alert. Returns true when allowed.
|
|
60
|
+
*
|
|
61
|
+
* if (!this.requireAuth(() => this.canDelete(), 'delete')) return;
|
|
62
|
+
*/
|
|
63
|
+
protected requireAuth(check: () => boolean, verb: AuthVerb): boolean {
|
|
64
|
+
if (check()) return true;
|
|
65
|
+
alert(`Missing authorization to ${verb} records`);
|
|
66
|
+
return false;
|
|
57
67
|
}
|
|
58
68
|
|
|
59
69
|
/**
|
|
@@ -62,7 +72,7 @@ export abstract class BaseTable {
|
|
|
62
72
|
* when the table has no custom actions.
|
|
63
73
|
*/
|
|
64
74
|
getActions(recordSpecific?: boolean): TableAction[] {
|
|
65
|
-
const tableDef = this.cacheService.getTableDefinition(this.tableName);
|
|
75
|
+
const tableDef = this.cacheService.getTableDefinition(this.tableName());
|
|
66
76
|
const actions = tableDef?.Actions ?? [];
|
|
67
77
|
if (recordSpecific === undefined) return [...actions].sort(this.sortActions);
|
|
68
78
|
return actions.filter((a) => a.recordSpecific === recordSpecific).sort(this.sortActions);
|
|
@@ -78,7 +88,56 @@ export abstract class BaseTable {
|
|
|
78
88
|
* action button should render.
|
|
79
89
|
*/
|
|
80
90
|
canExecuteAction(action: TableAction): boolean {
|
|
81
|
-
return this.cacheService.canExecute(action.authorityObject, action.authorityCheck, this.tableName);
|
|
91
|
+
return this.cacheService.canExecute(action.authorityObject, action.authorityCheck, this.tableName());
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Fire a TableAction. Template method:
|
|
96
|
+
*
|
|
97
|
+
* 1. Authorization check + alert (shared)
|
|
98
|
+
* 2. Subclass-specific pre-flight guard via `beforeExecuteAction()` —
|
|
99
|
+
* returns false to abort silently (the hook has already shown its
|
|
100
|
+
* own alert).
|
|
101
|
+
* 3. Optional confirm prompt from `action.confirmMessage`
|
|
102
|
+
* 4. POST `{}` for table-level actions or `primaryKeyValues(record)`
|
|
103
|
+
* for record-specific actions
|
|
104
|
+
* 5. On success, dispatch `onActionSuccess()` so subclasses can refresh
|
|
105
|
+
* their views; on error, log and alert.
|
|
106
|
+
*
|
|
107
|
+
* Subclasses typically only override the two hooks.
|
|
108
|
+
*/
|
|
109
|
+
executeAction(action: TableAction, record?: Record<string, unknown>): void {
|
|
110
|
+
if (!this.canExecuteAction(action)) {
|
|
111
|
+
alert(`Missing authorization for action ${action.authorityObject}/${action.authorityCheck}`);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (!this.beforeExecuteAction(action, record)) return;
|
|
115
|
+
if (action.confirmMessage && !confirm(action.confirmMessage)) return;
|
|
116
|
+
const body = action.recordSpecific && record ? this.primaryKeyValues(record) : {};
|
|
117
|
+
this.backendService.executeAction(action.method, body).subscribe({
|
|
118
|
+
next: () => this.onActionSuccess(action, record),
|
|
119
|
+
error: (err) => {
|
|
120
|
+
console.error(`Action ${action.action} failed`, err);
|
|
121
|
+
alert(err?.error?.detail ?? `Failed to ${action.caption}`);
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Pre-flight hook for `executeAction()`. Return false to abort the action
|
|
128
|
+
* (the override should `alert()` or otherwise surface why). Default: allow.
|
|
129
|
+
*/
|
|
130
|
+
protected beforeExecuteAction(_action: TableAction, _record?: Record<string, unknown>): boolean {
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Post-success hook for `executeAction()`. Override to refresh the screen
|
|
136
|
+
* — e.g. `TableList` re-fetches the row list. Default: no-op (screens
|
|
137
|
+
* driven by parent state, like `TableDetail`, leave refresh to the parent).
|
|
138
|
+
*/
|
|
139
|
+
protected onActionSuccess(_action: TableAction, _record?: Record<string, unknown>): void {
|
|
140
|
+
// intentionally empty
|
|
82
141
|
}
|
|
83
142
|
|
|
84
143
|
/**
|
|
@@ -89,7 +148,7 @@ export abstract class BaseTable {
|
|
|
89
148
|
* does for get / delete.
|
|
90
149
|
*/
|
|
91
150
|
primaryKeyValues(record: Record<string, unknown>): Record<string, unknown> {
|
|
92
|
-
const tableDef = this.cacheService.getTableDefinition(this.tableName);
|
|
151
|
+
const tableDef = this.cacheService.getTableDefinition(this.tableName());
|
|
93
152
|
const pk: Record<string, unknown> = {};
|
|
94
153
|
if (!tableDef?.Keys) return pk;
|
|
95
154
|
for (const key of tableDef.Keys) {
|
|
@@ -99,9 +158,9 @@ export abstract class BaseTable {
|
|
|
99
158
|
}
|
|
100
159
|
|
|
101
160
|
emptyRecord(): any {
|
|
102
|
-
const record : any = {[this.config.opField]:
|
|
103
|
-
const tableDef = this.cacheService.getTableDefinition(this.tableName);
|
|
104
|
-
const tableOverride = this.config.tableOverrides?.[this.tableName];
|
|
161
|
+
const record : any = {[this.config.opField]: OpCode.Insert}
|
|
162
|
+
const tableDef = this.cacheService.getTableDefinition(this.tableName());
|
|
163
|
+
const tableOverride = this.config.tableOverrides?.[this.tableName()];
|
|
105
164
|
const editableTimestamps = new Set(tableOverride?.editableTimestamps ?? []);
|
|
106
165
|
|
|
107
166
|
if (tableDef) {
|
|
@@ -141,8 +200,8 @@ export abstract class BaseTable {
|
|
|
141
200
|
}
|
|
142
201
|
|
|
143
202
|
getDisplayedColumns(records: any[] = []): string[] {
|
|
144
|
-
const tableDef = this.cacheService.getTableDefinition(this.tableName);
|
|
145
|
-
const tableOverride = this.config.tableOverrides?.[this.tableName];
|
|
203
|
+
const tableDef = this.cacheService.getTableDefinition(this.tableName());
|
|
204
|
+
const tableOverride = this.config.tableOverrides?.[this.tableName()];
|
|
146
205
|
const hidden = new Set([
|
|
147
206
|
...this.config.hiddenFields,
|
|
148
207
|
...(tableOverride?.hiddenFields ?? []),
|
|
@@ -162,7 +221,7 @@ export abstract class BaseTable {
|
|
|
162
221
|
}
|
|
163
222
|
|
|
164
223
|
getColumn(fieldName: string): TableColumn | undefined {
|
|
165
|
-
const tableDef = this.cacheService.getTableDefinition(this.tableName);
|
|
224
|
+
const tableDef = this.cacheService.getTableDefinition(this.tableName());
|
|
166
225
|
if (tableDef && tableDef.Columns) {
|
|
167
226
|
return tableDef.Columns.find((field) => field.PascalName === fieldName);
|
|
168
227
|
}
|
|
@@ -178,7 +237,7 @@ export abstract class BaseTable {
|
|
|
178
237
|
const match = options?.find(o => o.Value === String(rawValue));
|
|
179
238
|
if (match) return match.Caption ?? String(rawValue);
|
|
180
239
|
}
|
|
181
|
-
if (col.LookupTable && col.LookupStyle ===
|
|
240
|
+
if (col.LookupTable && col.LookupStyle === LookupStyle.Dropdown) {
|
|
182
241
|
const options = this.cacheService.getTableValues(col.LookupTable);
|
|
183
242
|
const match = options?.find(o => o.Value === String(rawValue));
|
|
184
243
|
if (match) return match.Caption ?? String(rawValue);
|
|
@@ -261,7 +320,7 @@ export abstract class BaseTable {
|
|
|
261
320
|
readyToSave(record: any, original?: any) {
|
|
262
321
|
this.restoreRecordTimeStamp(record);
|
|
263
322
|
const op = record[this.config.opField];
|
|
264
|
-
if (op ===
|
|
323
|
+
if (op === OpCode.Insert || op === OpCode.Delete) return; // already marked
|
|
265
324
|
// Original snapshot may carry display-formatted timestamps; restore both
|
|
266
325
|
// sides to the same wire format before comparing, otherwise scalar
|
|
267
326
|
// fields like Passdate look "modified" purely due to formatting.
|
|
@@ -269,11 +328,11 @@ export abstract class BaseTable {
|
|
|
269
328
|
const normalized = { ...original };
|
|
270
329
|
this.restoreRecordTimeStamp(normalized);
|
|
271
330
|
if (!this.isRecordDirty(record, normalized)) {
|
|
272
|
-
record[this.config.opField] =
|
|
331
|
+
record[this.config.opField] = OpCode.Readonly;
|
|
273
332
|
return;
|
|
274
333
|
}
|
|
275
334
|
}
|
|
276
|
-
record[this.config.opField] =
|
|
335
|
+
record[this.config.opField] = OpCode.Update;
|
|
277
336
|
}
|
|
278
337
|
|
|
279
338
|
/** Shallow compare of own scalar fields, ignoring child arrays and op_code. */
|
|
@@ -290,11 +349,11 @@ export abstract class BaseTable {
|
|
|
290
349
|
}
|
|
291
350
|
|
|
292
351
|
getDialogData(record: any, isNew: boolean, readOnlyColumns: string[] = []): any {
|
|
293
|
-
return {record, tableName: this.tableName, isNew, readOnlyColumns};
|
|
352
|
+
return {record, tableName: this.tableName(), isNew, readOnlyColumns};
|
|
294
353
|
}
|
|
295
354
|
|
|
296
355
|
getTableName(fieldName: string): string {
|
|
297
|
-
const tableDef = this.cacheService.getTableDefinition(this.tableName);
|
|
356
|
+
const tableDef = this.cacheService.getTableDefinition(this.tableName());
|
|
298
357
|
if (!tableDef) return '';
|
|
299
358
|
const fk = tableDef.Children?.find((c) => c.PascalName === fieldName);
|
|
300
359
|
return fk ? fk.ChildTable : '';
|
|
@@ -308,14 +367,14 @@ export abstract class BaseTable {
|
|
|
308
367
|
if (col && col.LookupDomain) {
|
|
309
368
|
return this.cacheService.getDomainValues(col.LookupDomain);
|
|
310
369
|
}
|
|
311
|
-
if (col && col.LookupTable && col.LookupStyle ===
|
|
370
|
+
if (col && col.LookupTable && col.LookupStyle === LookupStyle.Dropdown) {
|
|
312
371
|
return this.cacheService.getTableValues(col.LookupTable);
|
|
313
372
|
}
|
|
314
373
|
return undefined;
|
|
315
374
|
}
|
|
316
375
|
|
|
317
376
|
getKeyFilters(record: any): {[key: string]: string} | undefined {
|
|
318
|
-
const tableDef = this.cacheService.getTableDefinition(this.tableName);
|
|
377
|
+
const tableDef = this.cacheService.getTableDefinition(this.tableName());
|
|
319
378
|
if (!tableDef || !tableDef.Keys || tableDef.Keys.length === 0) {
|
|
320
379
|
return undefined;
|
|
321
380
|
}
|
|
@@ -328,7 +387,7 @@ export abstract class BaseTable {
|
|
|
328
387
|
|
|
329
388
|
getForeignKeyConfig(parentTable: string): {fk: ForeignKey, parent: TableDefinition} | null{
|
|
330
389
|
if (!parentTable) return null;
|
|
331
|
-
const tableDef = this.cacheService.getTableDefinition(this.tableName);
|
|
390
|
+
const tableDef = this.cacheService.getTableDefinition(this.tableName());
|
|
332
391
|
const parent = this.cacheService.getTableDefinition(parentTable);
|
|
333
392
|
if (!tableDef || !tableDef.Keys || !tableDef.Parents || !parent) return null;
|
|
334
393
|
const fk = tableDef.Parents.find((parent) => parent.ParentTable === parentTable);
|
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import { Directive, inject,
|
|
1
|
+
import { Directive, inject, signal, WritableSignal } from "@angular/core";
|
|
2
2
|
import { BaseTable } from "./base_table";
|
|
3
3
|
import { MatDialog } from "@angular/material/dialog";
|
|
4
4
|
|
|
5
5
|
@Directive()
|
|
6
6
|
export abstract class BaseView extends BaseTable {
|
|
7
7
|
protected readonly dialog = inject(MatDialog);
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
@Input() dialogComponent: any;
|
|
8
|
+
readonly apiName: WritableSignal<string> = signal('');
|
|
9
|
+
readonly dialogComponent: WritableSignal<any> = signal(undefined);
|
|
11
10
|
records: Array<{[key: string]: any}> = [];
|
|
12
11
|
displayedColumns: string[] = [];
|
|
13
12
|
searchTerms: {[key: string]: string} = {};
|
|
@@ -31,70 +30,58 @@ export abstract class BaseView extends BaseTable {
|
|
|
31
30
|
}
|
|
32
31
|
|
|
33
32
|
trackByRecord(index: number, record: {[key: string]: any}): any {
|
|
34
|
-
const tableDef = this.cacheService.getTableDefinition(this.tableName);
|
|
33
|
+
const tableDef = this.cacheService.getTableDefinition(this.tableName());
|
|
35
34
|
if (!tableDef || !tableDef.Keys || tableDef.Keys.length === 0) return index;
|
|
36
35
|
return tableDef.Keys.map((key) => record[key.PascalName]).join('_');
|
|
37
36
|
}
|
|
38
37
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
});
|
|
50
|
-
}
|
|
38
|
+
/**
|
|
39
|
+
* Shared row-dialog opener. Builds the dialog data and wires the result
|
|
40
|
+
* straight into `handleUpdate()` (subclass-specific). Used by both
|
|
41
|
+
* `addRecord()` (new row + empty `original`) and `editRecord()` (existing
|
|
42
|
+
* row passed through verbatim).
|
|
43
|
+
*/
|
|
44
|
+
private openRecordDialog(record: {[key: string]: any}, isNew: boolean): void {
|
|
45
|
+
const dialogData = this.getDialogData(record, isNew);
|
|
46
|
+
const apiName = this.apiName();
|
|
47
|
+
if (apiName) dialogData['apiName'] = apiName;
|
|
51
48
|
|
|
52
|
-
|
|
49
|
+
this.dialog.open(this.dialogComponent(), {
|
|
53
50
|
width: '90vw',
|
|
54
51
|
maxWidth: '1200px',
|
|
55
52
|
maxHeight: '90vh',
|
|
56
53
|
disableClose: true,
|
|
57
|
-
data:
|
|
54
|
+
data: dialogData,
|
|
55
|
+
}).afterClosed().subscribe((result) => {
|
|
56
|
+
if (result) this.handleUpdate(result, isNew, isNew ? {} : record);
|
|
58
57
|
});
|
|
58
|
+
}
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
addRecord() {
|
|
61
|
+
if (!this.requireAuth(() => this.canCreate(), 'create')) return;
|
|
62
|
+
const record = this.emptyRecord();
|
|
63
|
+
// Seed columns from an existing row when emptyRecord() returned just the
|
|
64
|
+
// op-code field (table metadata wasn't loaded yet).
|
|
65
|
+
if (Object.keys(record).length === 1 && this.records.length > 0) {
|
|
66
|
+
Object.keys(this.records[0]).forEach((key) => { record[key] = ''; });
|
|
67
|
+
}
|
|
68
|
+
this.openRecordDialog(record, true);
|
|
63
69
|
}
|
|
64
70
|
|
|
65
71
|
/**
|
|
66
72
|
* Open the row dialog. Renamed conceptually from "edit": when the user
|
|
67
|
-
* has only `canRead()
|
|
73
|
+
* has only `canRead()`, we still want to show them the record + child rows.
|
|
68
74
|
* The dialog itself (TableEdit) handles read-only mode — its Edit/New/Delete
|
|
69
75
|
* buttons are gated by the same permissions.
|
|
70
76
|
* Block only when neither read nor update is granted.
|
|
71
77
|
*/
|
|
72
78
|
editRecord(record: {[key: string]: any}) {
|
|
73
|
-
if (!this.canUpdate() && !this.canRead())
|
|
74
|
-
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
const dialogData = this.getDialogData(record, false);
|
|
78
|
-
if (this.apiName) {
|
|
79
|
-
dialogData['apiName'] = this.apiName;
|
|
80
|
-
}
|
|
81
|
-
const dialogRef = this.dialog.open(this.dialogComponent, {
|
|
82
|
-
width: '90vw',
|
|
83
|
-
maxWidth: '1200px',
|
|
84
|
-
maxHeight: '90vh',
|
|
85
|
-
disableClose: true,
|
|
86
|
-
data: dialogData,
|
|
87
|
-
});
|
|
88
|
-
dialogRef.afterClosed().subscribe((result) => {
|
|
89
|
-
if (result) this.handleUpdate(result, false, record);
|
|
90
|
-
});
|
|
79
|
+
if (!this.canUpdate() && !this.requireAuth(() => this.canRead(), 'view')) return;
|
|
80
|
+
this.openRecordDialog(record, false);
|
|
91
81
|
}
|
|
92
82
|
|
|
93
83
|
deleteRecord(record: {[key: string]: any}) {
|
|
94
|
-
if (!this.canDelete())
|
|
95
|
-
alert('Missing authorization to delete records');
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
84
|
+
if (!this.requireAuth(() => this.canDelete(), 'delete')) return;
|
|
98
85
|
this.handleDelete(record);
|
|
99
86
|
}
|
|
100
87
|
}
|
|
@@ -13,7 +13,7 @@ import { CheckoutMode } from '../../model/appdata';
|
|
|
13
13
|
* rejected with 400.
|
|
14
14
|
*/
|
|
15
15
|
@Component({
|
|
16
|
-
selector: '
|
|
16
|
+
selector: 'sail-checkout-button',
|
|
17
17
|
templateUrl: './checkout_button.html',
|
|
18
18
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
19
19
|
encapsulation: ViewEncapsulation.None,
|
|
@@ -11,7 +11,7 @@ import { PaymentMethod } from '../../model/appdata';
|
|
|
11
11
|
* `[portalUrl]` to show it; otherwise the button is hidden.
|
|
12
12
|
*/
|
|
13
13
|
@Component({
|
|
14
|
-
selector: '
|
|
14
|
+
selector: 'sail-payment-methods',
|
|
15
15
|
templateUrl: './payment_methods.html',
|
|
16
16
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
17
17
|
encapsulation: ViewEncapsulation.None,
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
}
|
|
14
14
|
</ul>
|
|
15
15
|
}
|
|
16
|
-
<button
|
|
16
|
+
<button matButton="elevated" class="primary" type="button" (click)="onSelect(plan.id)">
|
|
17
17
|
@if (plan.id === selected()) { Selected } @else { Choose }
|
|
18
18
|
</button>
|
|
19
19
|
</div>
|
|
@@ -7,7 +7,7 @@ import { PublicPlan } from '../../model/appdata';
|
|
|
7
7
|
* The caller provides the plans and handles the selection event.
|
|
8
8
|
*/
|
|
9
9
|
@Component({
|
|
10
|
-
selector: '
|
|
10
|
+
selector: 'sail-plan-selector',
|
|
11
11
|
templateUrl: './plan_selector.html',
|
|
12
12
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
13
13
|
encapsulation: ViewEncapsulation.None,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<mat-form-field [appearance]="appearance()" [subscriptSizing]="subscriptSizing()">
|
|
3
3
|
<mat-label>{{ colCaption(field()) }}</mat-label>
|
|
4
4
|
<input matInput [value]="readonlyField() ? displayCaption : value()" [required]="required" [readonly]="readonlyField()">
|
|
5
|
-
<button
|
|
5
|
+
<button matIconButton matSuffix (click)="openLookup()" [disabled]="readonlyField()">
|
|
6
6
|
<mat-icon>search</mat-icon>
|
|
7
7
|
</button>
|
|
8
8
|
</mat-form-field>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ChangeDetectionStrategy, Component, inject,
|
|
1
|
+
import { ChangeDetectionStrategy, Component, inject, input, linkedSignal, output, ViewEncapsulation } from "@angular/core";
|
|
2
2
|
import { MatButtonModule } from "@angular/material/button";
|
|
3
3
|
import { MatCheckboxModule } from "@angular/material/checkbox";
|
|
4
4
|
import { MatDialog } from "@angular/material/dialog";
|
|
@@ -7,11 +7,11 @@ import { MatIconModule } from "@angular/material/icon";
|
|
|
7
7
|
import { MatInputModule } from "@angular/material/input";
|
|
8
8
|
import { MatSelectModule } from "@angular/material/select";
|
|
9
9
|
import { BaseTable } from "../abstract/base_table";
|
|
10
|
-
import { ConstantValue } from "../../model/common";
|
|
10
|
+
import { ConstantValue, LookupStyle } from "../../model/common";
|
|
11
11
|
import { TableLookup } from "../table/table_lookup";
|
|
12
12
|
|
|
13
13
|
@Component({
|
|
14
|
-
selector: 'dynamic-field',
|
|
14
|
+
selector: 'sail-dynamic-field',
|
|
15
15
|
templateUrl: './form_field.html',
|
|
16
16
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
17
17
|
encapsulation: ViewEncapsulation.None,
|
|
@@ -31,8 +31,7 @@ import { TableLookup } from "../table/table_lookup";
|
|
|
31
31
|
]
|
|
32
32
|
})
|
|
33
33
|
export class DynamicField extends BaseTable {
|
|
34
|
-
|
|
35
|
-
@Input() override tableName = '';
|
|
34
|
+
protected readonly tableNameInput = input('', { alias: 'tableName' });
|
|
36
35
|
readonly value = input<any>(undefined);
|
|
37
36
|
readonly valueChange = output<any>();
|
|
38
37
|
readonly recordUpdate = output<{[key: string]: any}>();
|
|
@@ -42,6 +41,8 @@ export class DynamicField extends BaseTable {
|
|
|
42
41
|
readonly subscriptSizing = input<'fixed' | 'dynamic'>('dynamic');
|
|
43
42
|
readonly appearance = input<'fill' | 'outline'>('outline');
|
|
44
43
|
|
|
44
|
+
override readonly tableName = linkedSignal<string, string>({ source: () => this.tableNameInput(), computation: (v, p) => v || p?.value || '' });
|
|
45
|
+
|
|
45
46
|
private readonly dialog = inject(MatDialog);
|
|
46
47
|
|
|
47
48
|
get col() {
|
|
@@ -49,7 +50,7 @@ export class DynamicField extends BaseTable {
|
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
get inputType() {
|
|
52
|
-
if (!this.col || !this.col.InputType || this.col.LookupStyle ===
|
|
53
|
+
if (!this.col || !this.col.InputType || this.col.LookupStyle === LookupStyle.Search) return 'text';
|
|
53
54
|
return this.col.InputType;
|
|
54
55
|
}
|
|
55
56
|
|
|
@@ -86,7 +87,7 @@ export class DynamicField extends BaseTable {
|
|
|
86
87
|
}
|
|
87
88
|
|
|
88
89
|
isLookupTableSearch(): boolean {
|
|
89
|
-
return !!(this.col && this.col.LookupTable && this.col.LookupStyle ===
|
|
90
|
+
return !!(this.col && this.col.LookupTable && this.col.LookupStyle === LookupStyle.Search);
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
get maxLength() {
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
<div class="form-container">
|
|
2
2
|
@for (key of computedFields(); track key) {
|
|
3
3
|
@if (!isArray(record()[key])) {
|
|
4
|
-
<dynamic-field
|
|
4
|
+
<sail-dynamic-field
|
|
5
5
|
[tableName]="tableName()"
|
|
6
6
|
[value]="record()[key]"
|
|
7
7
|
(valueChange)="record()[key]=$event"
|
|
8
8
|
(recordUpdate)="onRecordUpdate($event)"
|
|
9
9
|
[field]="key"
|
|
10
10
|
[readonly]="isReadOnly()(key)">
|
|
11
|
-
</dynamic-field>
|
|
11
|
+
</sail-dynamic-field>
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
14
|
</div>
|
|
@@ -4,7 +4,7 @@ import { BaseAuthService } from "../../service/auth.service";
|
|
|
4
4
|
import { SAIL_GUI_CONFIG, SailGuiConfig, DEFAULT_CONFIG } from "../../config";
|
|
5
5
|
|
|
6
6
|
@Component({
|
|
7
|
-
selector: 'record-form',
|
|
7
|
+
selector: 'sail-record-form',
|
|
8
8
|
templateUrl: './form_record.html',
|
|
9
9
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
10
10
|
encapsulation: ViewEncapsulation.None,
|
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
{{ getCaption() }} Details
|
|
7
7
|
}
|
|
8
8
|
</span>
|
|
9
|
-
<button
|
|
9
|
+
<button matIconButton type="button" (click)="onCancel()" aria-label="Close dialog" class="table-form-close">
|
|
10
10
|
<mat-icon>close</mat-icon>
|
|
11
11
|
</button>
|
|
12
12
|
</h1>
|
|
13
13
|
<div mat-dialog-content>
|
|
14
|
-
<record-form [tableName]="tableName" [record]="editableRecord" [isReadOnly]="isReadOnlyBound"></record-form>
|
|
14
|
+
<sail-record-form [tableName]="tableName()" [record]="editableRecord" [isReadOnly]="isReadOnlyBound"></sail-record-form>
|
|
15
15
|
</div>
|
|
16
16
|
<div mat-dialog-actions align="end">
|
|
17
|
-
<button
|
|
18
|
-
<button
|
|
17
|
+
<button matButton="elevated" class="primary" type="submit" (click)="onSave()">Save</button>
|
|
18
|
+
<button matButton type="button" (click)="onCancel()">Cancel</button>
|
|
19
19
|
</div>
|
|
@@ -14,7 +14,7 @@ interface TableFormDialogData {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
@Component({
|
|
17
|
-
selector: 'table-form',
|
|
17
|
+
selector: 'sail-table-form',
|
|
18
18
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
19
19
|
encapsulation: ViewEncapsulation.None,
|
|
20
20
|
templateUrl: './form_table.html',
|
|
@@ -36,7 +36,7 @@ export class TableForm extends BaseForm {
|
|
|
36
36
|
constructor() {
|
|
37
37
|
super();
|
|
38
38
|
this.editableRecord = {...this.data.record};
|
|
39
|
-
this.tableName
|
|
39
|
+
this.tableName.set(this.data.tableName);
|
|
40
40
|
this.isNew = !!this.data.isNew;
|
|
41
41
|
this.readOnlyColumns = this.data.readOnlyColumns ?? [];
|
|
42
42
|
}
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
<textarea matInput rows="3" formControlName="reason"></textarea>
|
|
24
24
|
</mat-form-field>
|
|
25
25
|
|
|
26
|
-
<button
|
|
26
|
+
<button matButton="elevated" class="warn" type="submit" [disabled]="!canSubmit()">
|
|
27
27
|
@if (loading()) { Deleting… } @else { Permanently delete account }
|
|
28
28
|
</button>
|
|
29
29
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ChangeDetectionStrategy, Component, ViewEncapsulation, computed, inject, input, signal,
|
|
3
3
|
} from '@angular/core';
|
|
4
|
+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
4
5
|
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
|
|
5
6
|
import { MatButtonModule } from '@angular/material/button';
|
|
6
7
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
|
@@ -23,7 +24,7 @@ import { SAIL_GUI_CONFIG, SailGuiConfig, DEFAULT_CONFIG } from '../../config';
|
|
|
23
24
|
* `/login/local`).
|
|
24
25
|
*/
|
|
25
26
|
@Component({
|
|
26
|
-
selector: '
|
|
27
|
+
selector: 'sail-account-deletion',
|
|
27
28
|
templateUrl: './account_deletion.html',
|
|
28
29
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
29
30
|
encapsulation: ViewEncapsulation.None,
|
|
@@ -59,7 +60,9 @@ export class AccountDeletionComponent extends BaseAsync {
|
|
|
59
60
|
|
|
60
61
|
constructor() {
|
|
61
62
|
super();
|
|
62
|
-
this.form.controls.confirm.valueChanges
|
|
63
|
+
this.form.controls.confirm.valueChanges
|
|
64
|
+
.pipe(takeUntilDestroyed())
|
|
65
|
+
.subscribe((v) => this.typedValue.set(v ?? ''));
|
|
63
66
|
}
|
|
64
67
|
|
|
65
68
|
onDelete() {
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
<div class="auth-success" role="status">{{ successMessage() }}</div>
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
<button
|
|
46
|
+
<button matButton="elevated" class="primary" type="submit" [disabled]="!chpassForm.valid">
|
|
47
47
|
@if (forgotPassword()) {
|
|
48
48
|
Send Confirmation Code
|
|
49
49
|
} @else {
|