@nauticana/sail 1.1.43 → 1.1.45
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 +31 -7
- package/package.json +1 -1
- package/src/agent/component/studio_page.ts +35 -20
- package/src/component/abstract/base_form.ts +5 -0
- package/src/component/abstract/base_table.ts +23 -29
- package/src/component/abstract/base_view.ts +26 -17
- package/src/component/account/profile_editor.html +15 -8
- package/src/component/account/profile_editor.ts +12 -7
- package/src/component/agency/agency_clients.html +4 -1
- package/src/component/agency/agency_clients.ts +6 -3
- package/src/component/dialog/confirm_dialog.ts +29 -0
- package/src/component/form/field_error.ts +26 -0
- package/src/component/form/form_field.html +5 -10
- package/src/component/form/form_field.ts +10 -0
- package/src/component/login/chpass_component.html +4 -1
- package/src/component/login/chpass_component.ts +6 -3
- package/src/component/login/confirm_chpass_component.html +4 -1
- package/src/component/login/confirm_chpass_component.ts +6 -3
- package/src/component/login/confirm_register_component.html +3 -1
- package/src/component/login/confirm_register_component.ts +6 -3
- package/src/component/login/login_component.html +3 -1
- package/src/component/login/login_component.ts +6 -3
- package/src/component/login/register_component.html +14 -1
- package/src/component/login/register_component.ts +6 -3
- package/src/component/navigation/navigation.html +4 -1
- package/src/component/navigation/navigation.ts +24 -5
- package/src/component/oauth_connection/oauth_connection_panel.html +4 -0
- package/src/component/oauth_connection/oauth_connection_panel.ts +9 -5
- package/src/component/payout/bank_info_form.html +6 -2
- package/src/component/payout/bank_info_form.ts +6 -3
- package/src/component/security/api_keys.html +6 -2
- package/src/component/security/api_keys.ts +13 -7
- package/src/component/security/connected_apps.ts +7 -5
- package/src/component/security/sessions.ts +11 -9
- package/src/component/security/trusted_devices.html +2 -1
- package/src/component/security/trusted_devices.ts +18 -13
- package/src/component/security/twofactor_setup.html +7 -3
- package/src/component/security/twofactor_setup.ts +8 -5
- package/src/component/security/twofactor_verify.html +2 -1
- package/src/component/security/twofactor_verify.ts +6 -3
- package/src/component/table/table_detail.ts +1 -1
- package/src/component/table/table_edit.ts +27 -5
- package/src/component/table/table_list.html +21 -5
- package/src/component/table/table_list.ts +74 -67
- package/src/component/table/table_search.ts +2 -2
- package/src/index.ts +4 -0
- package/src/model/appdata.ts +3 -0
- package/src/service/auth.service.ts +30 -17
- package/src/service/billing.service.ts +8 -6
- package/src/service/prompt.service.ts +27 -0
- package/src/util/forms.ts +12 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component, inject } from "@angular/core";
|
|
2
|
+
import { MAT_DIALOG_DATA, MatDialogModule } from "@angular/material/dialog";
|
|
3
|
+
import { MatButtonModule } from "@angular/material/button";
|
|
4
|
+
|
|
5
|
+
export interface ConfirmOptions {
|
|
6
|
+
title: string;
|
|
7
|
+
message?: string;
|
|
8
|
+
/** Label of the confirming button, e.g. "Delete record". */
|
|
9
|
+
action: string;
|
|
10
|
+
/** Styles the action with the global `.warn` class. */
|
|
11
|
+
destructive?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Material confirmation opened by `PromptService`; closes with true only on the action. */
|
|
15
|
+
@Component({
|
|
16
|
+
selector: 'sail-confirm-dialog',
|
|
17
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
18
|
+
imports: [MatDialogModule, MatButtonModule],
|
|
19
|
+
template: `
|
|
20
|
+
<h2 mat-dialog-title>{{ data.title }}</h2>
|
|
21
|
+
@if (data.message) { <mat-dialog-content><p>{{ data.message }}</p></mat-dialog-content> }
|
|
22
|
+
<mat-dialog-actions align="end">
|
|
23
|
+
<button matButton type="button" mat-dialog-close>Cancel</button>
|
|
24
|
+
<button matButton="filled" type="button" [class.warn]="data.destructive" [mat-dialog-close]="true">{{ data.action }}</button>
|
|
25
|
+
</mat-dialog-actions>`,
|
|
26
|
+
})
|
|
27
|
+
export class ConfirmDialog {
|
|
28
|
+
protected readonly data = inject<ConfirmOptions>(MAT_DIALOG_DATA);
|
|
29
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component, inject } from "@angular/core";
|
|
2
|
+
import { ValidationErrors } from "@angular/forms";
|
|
3
|
+
import { MatFormField } from "@angular/material/form-field";
|
|
4
|
+
|
|
5
|
+
/** Message for the enclosing field's first validation error: `<mat-error sailFieldError />`. */
|
|
6
|
+
@Component({
|
|
7
|
+
selector: 'mat-error[sailFieldError]',
|
|
8
|
+
// Default: control errors are not signals, so the message is re-read on every check.
|
|
9
|
+
changeDetection: ChangeDetectionStrategy.Default,
|
|
10
|
+
template: '{{ message }}',
|
|
11
|
+
})
|
|
12
|
+
export class FieldError {
|
|
13
|
+
private readonly formField = inject(MatFormField);
|
|
14
|
+
|
|
15
|
+
get message(): string {
|
|
16
|
+
const errors: ValidationErrors | null | undefined = this.formField._control?.ngControl?.errors;
|
|
17
|
+
if (!errors) return '';
|
|
18
|
+
if (errors['required']) return 'This field is required.';
|
|
19
|
+
if (errors['email']) return 'Enter a valid email address.';
|
|
20
|
+
if (errors['minlength']) return `Use at least ${errors['minlength'].requiredLength} characters.`;
|
|
21
|
+
if (errors['maxlength']) return `Use at most ${errors['maxlength'].requiredLength} characters.`;
|
|
22
|
+
if (errors['min']) return `Enter ${errors['min'].min} or more.`;
|
|
23
|
+
if (errors['max']) return `Enter ${errors['max'].max} or less.`;
|
|
24
|
+
return 'Check this value.';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
<mat-icon>search</mat-icon>
|
|
8
8
|
</button>
|
|
9
9
|
</mat-form-field>
|
|
10
|
-
} @else if (inputType === 'select' &&
|
|
10
|
+
} @else if (isReadonly && (inputType === 'select' || inputType === 'checkbox' || (isDateLike && !value()))) {
|
|
11
11
|
<mat-form-field [appearance]="appearance()" [subscriptSizing]="subscriptSizing()">
|
|
12
12
|
<mat-label>{{ colCaption(field()) }}</mat-label>
|
|
13
|
-
<input matInput [value]="
|
|
13
|
+
<input matInput [value]="readonlyText" readonly>
|
|
14
14
|
</mat-form-field>
|
|
15
15
|
} @else if (inputType === 'select') {
|
|
16
16
|
<mat-form-field [appearance]="appearance()" [subscriptSizing]="subscriptSizing()">
|
|
@@ -22,19 +22,14 @@
|
|
|
22
22
|
</mat-select>
|
|
23
23
|
</mat-form-field>
|
|
24
24
|
} @else if (inputType === 'checkbox') {
|
|
25
|
-
<mat-checkbox [checked]="
|
|
26
|
-
} @else if (isReadonly && isDateLike && !value()) {
|
|
27
|
-
<mat-form-field [appearance]="appearance()" [subscriptSizing]="subscriptSizing()">
|
|
28
|
-
<mat-label>{{ colCaption(field()) }}</mat-label>
|
|
29
|
-
<input matInput value="" readonly>
|
|
30
|
-
</mat-form-field>
|
|
25
|
+
<mat-checkbox [checked]="checked" (change)="onCheckboxChange($event);">{{ colCaption(field()) }}</mat-checkbox>
|
|
31
26
|
} @else if (isTextArea) {
|
|
32
27
|
<mat-form-field [appearance]="appearance()" [subscriptSizing]="subscriptSizing()">
|
|
33
28
|
<mat-label>{{ colCaption(field()) }}</mat-label>
|
|
34
29
|
<textarea matInput
|
|
35
30
|
[value]="inputValue"
|
|
36
31
|
(input)="onInputChange($event);"
|
|
37
|
-
[
|
|
32
|
+
[readonly]="isReadonly"
|
|
38
33
|
[required]="required"
|
|
39
34
|
[attr.maxLength]="maxLength"
|
|
40
35
|
[rows]="textAreaRows">
|
|
@@ -47,7 +42,7 @@
|
|
|
47
42
|
[type]="inputType"
|
|
48
43
|
[value]="inputValue"
|
|
49
44
|
(input)="onInputChange($event);"
|
|
50
|
-
[
|
|
45
|
+
[readonly]="isReadonly"
|
|
51
46
|
[required]="required"
|
|
52
47
|
[attr.maxLength]="maxLength"
|
|
53
48
|
[attr.step]="step">
|
|
@@ -140,6 +140,16 @@ export class DynamicField extends BaseTable {
|
|
|
140
140
|
return this.displayValue(this.field(), this.value());
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
get checked(): boolean {
|
|
144
|
+
return this.value() === true || this.value() === '1';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Text of a read-only select, boolean, or empty date; a checkbox has no native readonly state. */
|
|
148
|
+
get readonlyText(): string {
|
|
149
|
+
if (this.inputType === 'checkbox') return this.checked ? 'Yes' : 'No';
|
|
150
|
+
return this.inputType === 'select' ? this.displayCaption : '';
|
|
151
|
+
}
|
|
152
|
+
|
|
143
153
|
onInputChange(event: Event) {
|
|
144
154
|
const target = event.target as HTMLInputElement | HTMLTextAreaElement | null;
|
|
145
155
|
if (target) this.valueChange.emit(this.coerce(target.value));
|
|
@@ -10,12 +10,14 @@
|
|
|
10
10
|
<mat-form-field appearance="outline">
|
|
11
11
|
<mat-label>Username</mat-label>
|
|
12
12
|
<input matInput type="text" formControlName="username" autocomplete="username" required>
|
|
13
|
+
<mat-error sailFieldError></mat-error>
|
|
13
14
|
</mat-form-field>
|
|
14
15
|
|
|
15
16
|
@if (!forgotPassword()) {
|
|
16
17
|
<mat-form-field appearance="outline">
|
|
17
18
|
<mat-label>Current Password</mat-label>
|
|
18
19
|
<input matInput type="password" formControlName="old_password" autocomplete="current-password" required>
|
|
20
|
+
<mat-error sailFieldError></mat-error>
|
|
19
21
|
</mat-form-field>
|
|
20
22
|
|
|
21
23
|
<mat-form-field appearance="outline">
|
|
@@ -30,6 +32,7 @@
|
|
|
30
32
|
<mat-form-field appearance="outline">
|
|
31
33
|
<mat-label>Confirm New Password</mat-label>
|
|
32
34
|
<input matInput type="password" formControlName="confirm_password" autocomplete="new-password" required>
|
|
35
|
+
<mat-error sailFieldError></mat-error>
|
|
33
36
|
</mat-form-field>
|
|
34
37
|
}
|
|
35
38
|
|
|
@@ -43,7 +46,7 @@
|
|
|
43
46
|
<div class="auth-error" role="alert">{{ errorMessage() }}</div>
|
|
44
47
|
}
|
|
45
48
|
|
|
46
|
-
<button matButton="elevated" class="primary" type="submit"
|
|
49
|
+
<button matButton="elevated" class="primary" type="submit">
|
|
47
50
|
@if (forgotPassword()) {
|
|
48
51
|
Send Confirmation Code
|
|
49
52
|
} @else {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { ChangeDetectionStrategy, Component, computed, effect, inject, signal, ViewEncapsulation } from '@angular/core';
|
|
1
|
+
import { ChangeDetectionStrategy, Component, computed, effect, inject, signal, ViewEncapsulation, ElementRef } from '@angular/core';
|
|
2
|
+
import { submittable } from '../../util/forms';
|
|
3
|
+
import { FieldError } from '../form/field_error';
|
|
2
4
|
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
|
|
3
5
|
import { BaseAuthService } from '../../service/auth.service';
|
|
4
6
|
import { Router, RouterLink } from '@angular/router';
|
|
@@ -16,7 +18,7 @@ import { passwordPolicyValidator, passwordPolicyHint } from '../../util/password
|
|
|
16
18
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
17
19
|
encapsulation: ViewEncapsulation.None,
|
|
18
20
|
imports: [
|
|
19
|
-
ReactiveFormsModule,
|
|
21
|
+
FieldError, ReactiveFormsModule,
|
|
20
22
|
MatButtonModule,
|
|
21
23
|
MatFormFieldModule,
|
|
22
24
|
MatInputModule,
|
|
@@ -25,6 +27,7 @@ import { passwordPolicyValidator, passwordPolicyHint } from '../../util/password
|
|
|
25
27
|
],
|
|
26
28
|
})
|
|
27
29
|
export class ChpassComponent extends BaseAsync {
|
|
30
|
+
private readonly host: HTMLElement = inject(ElementRef).nativeElement;
|
|
28
31
|
private auth = inject(BaseAuthService);
|
|
29
32
|
private fb = inject(FormBuilder);
|
|
30
33
|
private router = inject(Router);
|
|
@@ -73,7 +76,7 @@ export class ChpassComponent extends BaseAsync {
|
|
|
73
76
|
}
|
|
74
77
|
|
|
75
78
|
changePassword(): void {
|
|
76
|
-
if (this.chpassForm.
|
|
79
|
+
if (!submittable(this.chpassForm, this.host)) return;
|
|
77
80
|
|
|
78
81
|
const { username, old_password, new_password, confirm_password } = this.chpassForm.value;
|
|
79
82
|
|
|
@@ -11,11 +11,13 @@
|
|
|
11
11
|
<mat-form-field appearance="outline">
|
|
12
12
|
<mat-label>Username</mat-label>
|
|
13
13
|
<input matInput type="text" formControlName="username" autocomplete="username" required>
|
|
14
|
+
<mat-error sailFieldError></mat-error>
|
|
14
15
|
</mat-form-field>
|
|
15
16
|
|
|
16
17
|
<mat-form-field appearance="outline">
|
|
17
18
|
<mat-label>Confirmation Code</mat-label>
|
|
18
19
|
<input matInput type="text" formControlName="code" autocomplete="one-time-code" required>
|
|
20
|
+
<mat-error sailFieldError></mat-error>
|
|
19
21
|
</mat-form-field>
|
|
20
22
|
|
|
21
23
|
<mat-form-field appearance="outline">
|
|
@@ -30,13 +32,14 @@
|
|
|
30
32
|
<mat-form-field appearance="outline">
|
|
31
33
|
<mat-label>Confirm New Password</mat-label>
|
|
32
34
|
<input matInput type="password" formControlName="confirm_password" autocomplete="new-password" required>
|
|
35
|
+
<mat-error sailFieldError></mat-error>
|
|
33
36
|
</mat-form-field>
|
|
34
37
|
|
|
35
38
|
@if (errorMessage()) {
|
|
36
39
|
<div class="auth-error" role="alert">{{ errorMessage() }}</div>
|
|
37
40
|
}
|
|
38
41
|
|
|
39
|
-
<button matButton="elevated" class="primary" type="submit"
|
|
42
|
+
<button matButton="elevated" class="primary" type="submit">
|
|
40
43
|
Confirm Password Change
|
|
41
44
|
</button>
|
|
42
45
|
</form>
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { ChangeDetectionStrategy, Component, computed, effect, inject, OnInit, ViewEncapsulation } from '@angular/core';
|
|
1
|
+
import { ChangeDetectionStrategy, Component, computed, effect, inject, OnInit, ViewEncapsulation, ElementRef } from '@angular/core';
|
|
2
|
+
import { submittable } from '../../util/forms';
|
|
3
|
+
import { FieldError } from '../form/field_error';
|
|
2
4
|
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
|
|
3
5
|
import { BaseAuthService } from '../../service/auth.service';
|
|
4
6
|
import { ActivatedRoute, RouterLink } from '@angular/router';
|
|
@@ -15,7 +17,7 @@ import { passwordPolicyValidator, passwordPolicyHint } from '../../util/password
|
|
|
15
17
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
16
18
|
encapsulation: ViewEncapsulation.None,
|
|
17
19
|
imports: [
|
|
18
|
-
ReactiveFormsModule,
|
|
20
|
+
FieldError, ReactiveFormsModule,
|
|
19
21
|
MatButtonModule,
|
|
20
22
|
MatFormFieldModule,
|
|
21
23
|
MatInputModule,
|
|
@@ -23,6 +25,7 @@ import { passwordPolicyValidator, passwordPolicyHint } from '../../util/password
|
|
|
23
25
|
],
|
|
24
26
|
})
|
|
25
27
|
export class ConfirmChpassComponent extends BaseAsync implements OnInit {
|
|
28
|
+
private readonly host: HTMLElement = inject(ElementRef).nativeElement;
|
|
26
29
|
private auth = inject(BaseAuthService);
|
|
27
30
|
private fb = inject(FormBuilder);
|
|
28
31
|
private route = inject(ActivatedRoute);
|
|
@@ -55,7 +58,7 @@ export class ConfirmChpassComponent extends BaseAsync implements OnInit {
|
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
confirm(): void {
|
|
58
|
-
if (this.confirmForm.
|
|
61
|
+
if (!submittable(this.confirmForm, this.host)) return;
|
|
59
62
|
|
|
60
63
|
const { username, code, new_password, confirm_password } = this.confirmForm.value;
|
|
61
64
|
|
|
@@ -11,11 +11,13 @@
|
|
|
11
11
|
<mat-form-field appearance="outline">
|
|
12
12
|
<mat-label>Email</mat-label>
|
|
13
13
|
<input matInput type="email" formControlName="email" required>
|
|
14
|
+
<mat-error sailFieldError></mat-error>
|
|
14
15
|
</mat-form-field>
|
|
15
16
|
|
|
16
17
|
<mat-form-field appearance="outline">
|
|
17
18
|
<mat-label>Confirmation Code</mat-label>
|
|
18
19
|
<input matInput type="text" formControlName="code" required>
|
|
20
|
+
<mat-error sailFieldError></mat-error>
|
|
19
21
|
</mat-form-field>
|
|
20
22
|
|
|
21
23
|
@if (errorMessage()) {
|
|
@@ -26,7 +28,7 @@
|
|
|
26
28
|
<div class="auth-success" role="status">{{ successMessage() }}</div>
|
|
27
29
|
}
|
|
28
30
|
|
|
29
|
-
<button matButton="elevated" class="primary" type="submit"
|
|
31
|
+
<button matButton="elevated" class="primary" type="submit">
|
|
30
32
|
Confirm
|
|
31
33
|
</button>
|
|
32
34
|
</form>
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { ChangeDetectionStrategy, Component, inject, OnInit, ViewEncapsulation } from '@angular/core';
|
|
1
|
+
import { ChangeDetectionStrategy, Component, inject, OnInit, ViewEncapsulation, ElementRef } from '@angular/core';
|
|
2
|
+
import { submittable } from '../../util/forms';
|
|
3
|
+
import { FieldError } from '../form/field_error';
|
|
2
4
|
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
|
|
3
5
|
import { BaseAuthService } from '../../service/auth.service';
|
|
4
6
|
import { ActivatedRoute, RouterLink } from '@angular/router';
|
|
@@ -14,7 +16,7 @@ import { BaseAsync } from '../abstract/base_async';
|
|
|
14
16
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
15
17
|
encapsulation: ViewEncapsulation.None,
|
|
16
18
|
imports: [
|
|
17
|
-
ReactiveFormsModule,
|
|
19
|
+
FieldError, ReactiveFormsModule,
|
|
18
20
|
MatButtonModule,
|
|
19
21
|
MatFormFieldModule,
|
|
20
22
|
MatInputModule,
|
|
@@ -22,6 +24,7 @@ import { BaseAsync } from '../abstract/base_async';
|
|
|
22
24
|
],
|
|
23
25
|
})
|
|
24
26
|
export class ConfirmRegisterComponent extends BaseAsync implements OnInit {
|
|
27
|
+
private readonly host: HTMLElement = inject(ElementRef).nativeElement;
|
|
25
28
|
private auth = inject(BaseAuthService);
|
|
26
29
|
private fb = inject(FormBuilder);
|
|
27
30
|
private route = inject(ActivatedRoute);
|
|
@@ -40,7 +43,7 @@ export class ConfirmRegisterComponent extends BaseAsync implements OnInit {
|
|
|
40
43
|
}
|
|
41
44
|
|
|
42
45
|
confirm(): void {
|
|
43
|
-
if (this.confirmForm.
|
|
46
|
+
if (!submittable(this.confirmForm, this.host)) return;
|
|
44
47
|
|
|
45
48
|
const { email, code } = this.confirmForm.value;
|
|
46
49
|
|
|
@@ -30,15 +30,17 @@
|
|
|
30
30
|
<mat-form-field appearance="outline">
|
|
31
31
|
<mat-label>Username</mat-label>
|
|
32
32
|
<input matInput type="text" formControlName="username" autocomplete="username" required>
|
|
33
|
+
<mat-error sailFieldError></mat-error>
|
|
33
34
|
</mat-form-field>
|
|
34
35
|
<mat-form-field appearance="outline">
|
|
35
36
|
<mat-label>Password</mat-label>
|
|
36
37
|
<input matInput type="password" formControlName="password" autocomplete="current-password" required>
|
|
38
|
+
<mat-error sailFieldError></mat-error>
|
|
37
39
|
</mat-form-field>
|
|
38
40
|
@if (errorMessage()) {
|
|
39
41
|
<p class="auth-error" role="alert">{{ errorMessage() }}</p>
|
|
40
42
|
}
|
|
41
|
-
<button matButton="elevated" class="primary" type="submit" [disabled]="
|
|
43
|
+
<button matButton="elevated" class="primary" type="submit" [disabled]="loading()">
|
|
42
44
|
{{ loading() ? 'Signing in…' : 'Login' }}
|
|
43
45
|
</button>
|
|
44
46
|
</form>
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { ChangeDetectionStrategy, Component, computed, inject, ViewEncapsulation } from '@angular/core';
|
|
1
|
+
import { ChangeDetectionStrategy, Component, computed, inject, ViewEncapsulation, ElementRef } from '@angular/core';
|
|
2
|
+
import { submittable } from '../../util/forms';
|
|
3
|
+
import { FieldError } from '../form/field_error';
|
|
2
4
|
import { BaseAsync } from '../abstract/base_async';
|
|
3
5
|
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
|
|
4
6
|
import { BaseAuthService } from '../../service/auth.service';
|
|
@@ -17,7 +19,7 @@ import { SsoLoginComponent } from '../sso/sso_login';
|
|
|
17
19
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
18
20
|
encapsulation: ViewEncapsulation.None,
|
|
19
21
|
imports: [
|
|
20
|
-
ReactiveFormsModule,
|
|
22
|
+
FieldError, ReactiveFormsModule,
|
|
21
23
|
MatButtonModule,
|
|
22
24
|
MatFormFieldModule,
|
|
23
25
|
MatInputModule,
|
|
@@ -27,6 +29,7 @@ import { SsoLoginComponent } from '../sso/sso_login';
|
|
|
27
29
|
],
|
|
28
30
|
})
|
|
29
31
|
export class LoginComponent extends BaseAsync {
|
|
32
|
+
private readonly host: HTMLElement = inject(ElementRef).nativeElement;
|
|
30
33
|
protected readonly auth = inject(BaseAuthService);
|
|
31
34
|
protected fb = inject(FormBuilder);
|
|
32
35
|
protected readonly guiConfig: SailGuiConfig = inject(SAIL_GUI_CONFIG, {optional: true}) ?? DEFAULT_CONFIG;
|
|
@@ -58,7 +61,7 @@ export class LoginComponent extends BaseAsync {
|
|
|
58
61
|
});
|
|
59
62
|
|
|
60
63
|
login(): void {
|
|
61
|
-
if (!this.loginForm.
|
|
64
|
+
if (!submittable(this.loginForm, this.host)) return;
|
|
62
65
|
const { username, password } = this.loginForm.value;
|
|
63
66
|
this.run(
|
|
64
67
|
this.auth.login(username!, password!),
|
|
@@ -14,11 +14,13 @@
|
|
|
14
14
|
<mat-form-field appearance="outline">
|
|
15
15
|
<mat-label>First Name</mat-label>
|
|
16
16
|
<input matInput type="text" formControlName="FirstName" autocomplete="given-name" required>
|
|
17
|
+
<mat-error sailFieldError></mat-error>
|
|
17
18
|
</mat-form-field>
|
|
18
19
|
|
|
19
20
|
<mat-form-field appearance="outline">
|
|
20
21
|
<mat-label>Last Name</mat-label>
|
|
21
22
|
<input matInput type="text" formControlName="LastName" autocomplete="family-name" required>
|
|
23
|
+
<mat-error sailFieldError></mat-error>
|
|
22
24
|
</mat-form-field>
|
|
23
25
|
</div>
|
|
24
26
|
|
|
@@ -33,6 +35,7 @@
|
|
|
33
35
|
<mat-form-field appearance="outline">
|
|
34
36
|
<mat-label>Username</mat-label>
|
|
35
37
|
<input matInput type="text" formControlName="UserName" autocomplete="username" required>
|
|
38
|
+
<mat-error sailFieldError></mat-error>
|
|
36
39
|
</mat-form-field>
|
|
37
40
|
|
|
38
41
|
<div class="form-row">
|
|
@@ -47,6 +50,7 @@
|
|
|
47
50
|
<mat-form-field appearance="outline">
|
|
48
51
|
<mat-label>Confirm Password</mat-label>
|
|
49
52
|
<input matInput type="password" formControlName="ConfirmPassword" autocomplete="new-password" required>
|
|
53
|
+
<mat-error sailFieldError></mat-error>
|
|
50
54
|
</mat-form-field>
|
|
51
55
|
</div>
|
|
52
56
|
@if (registerForm.hasError('passwordMismatch') && registerForm.controls.ConfirmPassword.touched) {
|
|
@@ -67,6 +71,7 @@
|
|
|
67
71
|
</mat-option>
|
|
68
72
|
}
|
|
69
73
|
</mat-select>
|
|
74
|
+
<mat-error sailFieldError></mat-error>
|
|
70
75
|
</mat-form-field>
|
|
71
76
|
|
|
72
77
|
<div class="auth-section-label">Business Information</div>
|
|
@@ -74,17 +79,20 @@
|
|
|
74
79
|
<mat-form-field appearance="outline">
|
|
75
80
|
<mat-label>Business/Company Name</mat-label>
|
|
76
81
|
<input matInput type="text" formControlName="PartnerCaption" required>
|
|
82
|
+
<mat-error sailFieldError></mat-error>
|
|
77
83
|
</mat-form-field>
|
|
78
84
|
|
|
79
85
|
<div class="form-row">
|
|
80
86
|
<mat-form-field appearance="outline">
|
|
81
87
|
<mat-label>Phone</mat-label>
|
|
82
88
|
<input matInput type="tel" formControlName="Phone" autocomplete="tel" required>
|
|
89
|
+
<mat-error sailFieldError></mat-error>
|
|
83
90
|
</mat-form-field>
|
|
84
91
|
|
|
85
92
|
<mat-form-field appearance="outline">
|
|
86
93
|
<mat-label>Website URL</mat-label>
|
|
87
94
|
<input matInput type="url" formControlName="DomainURL" autocomplete="url" required>
|
|
95
|
+
<mat-error sailFieldError></mat-error>
|
|
88
96
|
</mat-form-field>
|
|
89
97
|
</div>
|
|
90
98
|
|
|
@@ -93,17 +101,20 @@
|
|
|
93
101
|
<mat-form-field appearance="outline">
|
|
94
102
|
<mat-label>Address (Street and Number)</mat-label>
|
|
95
103
|
<input matInput type="text" formControlName="Address" autocomplete="address-line1" required>
|
|
104
|
+
<mat-error sailFieldError></mat-error>
|
|
96
105
|
</mat-form-field>
|
|
97
106
|
|
|
98
107
|
<div class="form-row">
|
|
99
108
|
<mat-form-field appearance="outline">
|
|
100
109
|
<mat-label>City</mat-label>
|
|
101
110
|
<input matInput type="text" formControlName="City" autocomplete="address-level2" required>
|
|
111
|
+
<mat-error sailFieldError></mat-error>
|
|
102
112
|
</mat-form-field>
|
|
103
113
|
|
|
104
114
|
<mat-form-field appearance="outline">
|
|
105
115
|
<mat-label>State</mat-label>
|
|
106
116
|
<input matInput type="text" formControlName="State" autocomplete="address-level1" required>
|
|
117
|
+
<mat-error sailFieldError></mat-error>
|
|
107
118
|
</mat-form-field>
|
|
108
119
|
</div>
|
|
109
120
|
|
|
@@ -111,11 +122,13 @@
|
|
|
111
122
|
<mat-form-field appearance="outline">
|
|
112
123
|
<mat-label>Zipcode</mat-label>
|
|
113
124
|
<input matInput type="text" formControlName="Zipcode" autocomplete="postal-code" required>
|
|
125
|
+
<mat-error sailFieldError></mat-error>
|
|
114
126
|
</mat-form-field>
|
|
115
127
|
|
|
116
128
|
<mat-form-field appearance="outline">
|
|
117
129
|
<mat-label>Country</mat-label>
|
|
118
130
|
<input matInput type="text" formControlName="Country" autocomplete="country-name" required>
|
|
131
|
+
<mat-error sailFieldError></mat-error>
|
|
119
132
|
</mat-form-field>
|
|
120
133
|
</div>
|
|
121
134
|
|
|
@@ -140,7 +153,7 @@
|
|
|
140
153
|
}
|
|
141
154
|
|
|
142
155
|
<button matButton="elevated" class="primary" type="submit"
|
|
143
|
-
[disabled]="
|
|
156
|
+
[disabled]="geocodeEnabled && geocodeStatus() !== 'success'">
|
|
144
157
|
Create Account
|
|
145
158
|
</button>
|
|
146
159
|
</form>
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { ChangeDetectionStrategy, Component, inject, DestroyRef, signal, ViewEncapsulation, OnInit } from '@angular/core';
|
|
1
|
+
import { ChangeDetectionStrategy, Component, inject, DestroyRef, signal, ViewEncapsulation, OnInit, ElementRef } from '@angular/core';
|
|
2
|
+
import { submittable } from '../../util/forms';
|
|
3
|
+
import { FieldError } from '../form/field_error';
|
|
2
4
|
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
|
|
3
5
|
import { AbstractControl, FormBuilder, ReactiveFormsModule, ValidationErrors, Validators } from '@angular/forms';
|
|
4
6
|
import { Router, RouterLink } from '@angular/router';
|
|
@@ -30,7 +32,7 @@ interface GeocodingResponse {
|
|
|
30
32
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
31
33
|
encapsulation: ViewEncapsulation.None,
|
|
32
34
|
imports: [
|
|
33
|
-
ReactiveFormsModule,
|
|
35
|
+
FieldError, ReactiveFormsModule,
|
|
34
36
|
RouterLink,
|
|
35
37
|
MatButtonModule,
|
|
36
38
|
MatFormFieldModule,
|
|
@@ -40,6 +42,7 @@ interface GeocodingResponse {
|
|
|
40
42
|
],
|
|
41
43
|
})
|
|
42
44
|
export class RegisterComponent implements OnInit {
|
|
45
|
+
private readonly host: HTMLElement = inject(ElementRef).nativeElement;
|
|
43
46
|
private auth = inject(BaseAuthService);
|
|
44
47
|
private billing = inject(BillingService);
|
|
45
48
|
private fb = inject(FormBuilder);
|
|
@@ -155,7 +158,7 @@ export class RegisterComponent implements OnInit {
|
|
|
155
158
|
}
|
|
156
159
|
|
|
157
160
|
register() {
|
|
158
|
-
if (!this.registerForm.
|
|
161
|
+
if (!submittable(this.registerForm, this.host)) return;
|
|
159
162
|
this.registerError.set('');
|
|
160
163
|
const formValue = this.registerForm.getRawValue();
|
|
161
164
|
const partnerReg: PartnerRegistration = {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
<a class="sail-skip-link" href="#sail-main" (click)="focusMain($event)">Skip to content</a>
|
|
1
2
|
<mat-sidenav-container class="sidenav-container">
|
|
2
3
|
<mat-sidenav #sidenav class="sidenav"
|
|
3
4
|
[attr.role]="isHandset() ? 'dialog' : 'navigation'"
|
|
@@ -61,6 +62,8 @@
|
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
64
|
</mat-toolbar>
|
|
64
|
-
<
|
|
65
|
+
<main #main id="sail-main" class="sail-main" tabindex="-1">
|
|
66
|
+
<router-outlet></router-outlet>
|
|
67
|
+
</main>
|
|
65
68
|
</mat-sidenav-content>
|
|
66
69
|
</mat-sidenav-container>
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { ChangeDetectionStrategy, Component, computed, inject, ViewEncapsulation } from "@angular/core";
|
|
1
|
+
import { ChangeDetectionStrategy, Component, computed, ElementRef, inject, viewChild, ViewEncapsulation } from "@angular/core";
|
|
2
2
|
import { NgOptimizedImage } from "@angular/common";
|
|
3
|
-
import { toSignal } from "@angular/core/rxjs-interop";
|
|
4
|
-
import { RouterLink, RouterOutlet } from "@angular/router";
|
|
5
|
-
import { MatSidenavModule } from '@angular/material/sidenav';
|
|
3
|
+
import { takeUntilDestroyed, toSignal } from "@angular/core/rxjs-interop";
|
|
4
|
+
import { NavigationEnd, Router, RouterLink, RouterOutlet } from "@angular/router";
|
|
5
|
+
import { MatSidenav, MatSidenavModule } from '@angular/material/sidenav';
|
|
6
6
|
import { MatToolbarModule } from '@angular/material/toolbar';
|
|
7
7
|
import { MatListModule } from '@angular/material/list';
|
|
8
8
|
import { MatIconModule } from '@angular/material/icon';
|
|
9
9
|
import { MatButtonModule } from '@angular/material/button';
|
|
10
10
|
import { MatExpansionModule } from '@angular/material/expansion';
|
|
11
11
|
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
|
|
12
|
-
import { map } from 'rxjs/operators';
|
|
12
|
+
import { filter, map, pairwise } from 'rxjs/operators';
|
|
13
13
|
import { ApplicationMenu, ApplicationMenuItem } from "../../model/common";
|
|
14
14
|
import { BaseAuthService } from "../../service/auth.service";
|
|
15
15
|
import { SAIL_GUI_CONFIG, SailGuiConfig, DEFAULT_CONFIG } from "../../config";
|
|
@@ -35,6 +35,8 @@ export class Navigation {
|
|
|
35
35
|
private readonly authService = inject(BaseAuthService);
|
|
36
36
|
private readonly breakpointObserver = inject(BreakpointObserver);
|
|
37
37
|
protected readonly guiConfig: SailGuiConfig = inject(SAIL_GUI_CONFIG, {optional: true}) ?? DEFAULT_CONFIG;
|
|
38
|
+
private readonly main = viewChild.required<ElementRef<HTMLElement>>('main');
|
|
39
|
+
private readonly sidenav = viewChild.required(MatSidenav);
|
|
38
40
|
|
|
39
41
|
// Restore the stored JWT before the menu stream fires so `canAccess()` checks
|
|
40
42
|
// resolve against the right user. Done at field-init time (runs in injection
|
|
@@ -61,6 +63,23 @@ export class Navigation {
|
|
|
61
63
|
readonly isLoggedIn = this.authService.isLoggedIn;
|
|
62
64
|
protected readonly accountLink = this.guiConfig.accountLink;
|
|
63
65
|
|
|
66
|
+
constructor() {
|
|
67
|
+
// Focus the page after a path change; the initial load and query-only updates keep focus,
|
|
68
|
+
// as does an open handset menu.
|
|
69
|
+
inject(Router).events.pipe(
|
|
70
|
+
filter((e): e is NavigationEnd => e instanceof NavigationEnd),
|
|
71
|
+
map((e) => e.urlAfterRedirects.split(/[?#]/)[0]),
|
|
72
|
+
pairwise(),
|
|
73
|
+
filter(([before, after]) => before !== after && !(this.isHandset() && this.sidenav().opened)),
|
|
74
|
+
takeUntilDestroyed(),
|
|
75
|
+
).subscribe(() => this.main().nativeElement.focus());
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
focusMain(event: Event) {
|
|
79
|
+
event.preventDefault();
|
|
80
|
+
this.main().nativeElement.focus();
|
|
81
|
+
}
|
|
82
|
+
|
|
64
83
|
logout() {
|
|
65
84
|
this.authService.logout();
|
|
66
85
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
@if (error()) {
|
|
2
2
|
<p class="connection-panel__error" role="alert">{{ error() }}</p>
|
|
3
3
|
}
|
|
4
|
+
<p class="connection-panel__notice" role="status">{{ notice() }}</p>
|
|
4
5
|
|
|
5
6
|
@if (loading()) {
|
|
6
7
|
<p>Loading connections…</p>
|
|
@@ -28,6 +29,9 @@
|
|
|
28
29
|
@if (v.provider.description) {
|
|
29
30
|
<p class="connection-card__desc">{{ v.provider.description }}</p>
|
|
30
31
|
}
|
|
32
|
+
@if (v.connection?.LastChecked) {
|
|
33
|
+
<p class="connection-card__checked">Last checked {{ v.connection?.LastChecked | date: 'medium' }}</p>
|
|
34
|
+
}
|
|
31
35
|
|
|
32
36
|
<div class="connection-card__actions">
|
|
33
37
|
@if (v.status === 'disconnected') {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ChangeDetectionStrategy, Component, DestroyRef, OnInit, computed, effect, inject, input, output, signal } from '@angular/core';
|
|
2
|
-
import { NgOptimizedImage } from '@angular/common';
|
|
2
|
+
import { DatePipe, NgOptimizedImage } from '@angular/common';
|
|
3
3
|
import { Subscription } from 'rxjs';
|
|
4
4
|
import { MatButtonModule } from '@angular/material/button';
|
|
5
5
|
import { MatCardModule } from '@angular/material/card';
|
|
@@ -22,7 +22,7 @@ import { OAuthConnectionStatus, OAuthConnectionView, PartnerCredential, OAuthPro
|
|
|
22
22
|
@Component({
|
|
23
23
|
selector: 'sail-oauth-connection-panel',
|
|
24
24
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
25
|
-
imports: [MatButtonModule, MatCardModule, MatIconModule, MatFormFieldModule, MatInputModule, NgOptimizedImage],
|
|
25
|
+
imports: [DatePipe, MatButtonModule, MatCardModule, MatIconModule, MatFormFieldModule, MatInputModule, NgOptimizedImage],
|
|
26
26
|
templateUrl: './oauth_connection_panel.html',
|
|
27
27
|
})
|
|
28
28
|
export class OAuthConnectionPanelComponent implements OnInit {
|
|
@@ -37,6 +37,7 @@ export class OAuthConnectionPanelComponent implements OnInit {
|
|
|
37
37
|
readonly connections = signal<PartnerCredential[]>([]);
|
|
38
38
|
readonly loading = signal(true);
|
|
39
39
|
readonly error = signal('');
|
|
40
|
+
readonly notice = signal(''); // last passing test
|
|
40
41
|
readonly expanded = signal(''); // provider code whose input form is open
|
|
41
42
|
readonly busy = signal(''); // provider code with an action in flight
|
|
42
43
|
private readonly fieldValues = signal<Record<string, Record<string, string>>>({});
|
|
@@ -98,7 +99,8 @@ export class OAuthConnectionPanelComponent implements OnInit {
|
|
|
98
99
|
return this.msg(e, 'Could not complete the connection.');
|
|
99
100
|
}
|
|
100
101
|
|
|
101
|
-
private reload(entityId: number): void {
|
|
102
|
+
private reload(entityId: number, keepNotice = false): void {
|
|
103
|
+
if (!keepNotice) this.notice.set('');
|
|
102
104
|
this.reloadSub?.unsubscribe(); // cancel any in-flight load so a slow old entity can't overwrite the new one
|
|
103
105
|
this.loading.set(true);
|
|
104
106
|
this.expanded.set('');
|
|
@@ -229,13 +231,15 @@ export class OAuthConnectionPanelComponent implements OnInit {
|
|
|
229
231
|
if (!v.connection) return;
|
|
230
232
|
this.busy.set(v.provider.code);
|
|
231
233
|
this.error.set('');
|
|
234
|
+
this.notice.set('');
|
|
232
235
|
this.svc.testConnection(v.provider.code, this.entityId()).subscribe({
|
|
233
236
|
// keel returns HTTP 200 with {status:'error', message} for a failed test —
|
|
234
237
|
// surface the message, then reload to reflect the status it just recorded.
|
|
235
238
|
next: (res) => {
|
|
236
239
|
this.busy.set('');
|
|
237
|
-
if (res.status
|
|
238
|
-
this.
|
|
240
|
+
if (res.status === 'ok') this.notice.set(`${v.provider.name} connection works.`);
|
|
241
|
+
else this.error.set(res.message || 'Connection test failed.');
|
|
242
|
+
this.reload(this.entityId(), true);
|
|
239
243
|
},
|
|
240
244
|
error: (e) => {
|
|
241
245
|
this.busy.set('');
|