mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-04-04 14:29:45 +00:00
Config refactor (#2128)
* prevent excessive nesting for unions, closes #2107, and genrally refactor config * a littel cleaner * working but with inefficiencies * remove warning from union list * introduce messaging for config with only pointers * feat(shared): `ElasticContainer` add new component (#2134) * feat(shared): `ElasticContainer` add new component * chore: fix imports * revert to 250 for resize * remove logs Co-authored-by: Alex Inkin <alexander@inkin.ru>
This commit is contained in:
committed by
Aiden McClelland
parent
c16404bb2d
commit
d223ac4675
63
frontend/package-lock.json
generated
63
frontend/package-lock.json
generated
@@ -18,6 +18,9 @@
|
|||||||
"@angular/router": "^14.1.0",
|
"@angular/router": "^14.1.0",
|
||||||
"@ionic/angular": "^6.1.15",
|
"@ionic/angular": "^6.1.15",
|
||||||
"@materia-ui/ngx-monaco-editor": "^6.0.0",
|
"@materia-ui/ngx-monaco-editor": "^6.0.0",
|
||||||
|
"@ng-web-apis/common": "^2.0.0",
|
||||||
|
"@ng-web-apis/mutation-observer": "^2.0.0",
|
||||||
|
"@ng-web-apis/resize-observer": "^2.0.0",
|
||||||
"@start9labs/argon2": "^0.1.0",
|
"@start9labs/argon2": "^0.1.0",
|
||||||
"@start9labs/emver": "^0.1.5",
|
"@start9labs/emver": "^0.1.5",
|
||||||
"angular-svg-round-progressbar": "^9.0.0",
|
"angular-svg-round-progressbar": "^9.0.0",
|
||||||
@@ -3266,6 +3269,42 @@
|
|||||||
"rxjs": ">=6.0.0"
|
"rxjs": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@ng-web-apis/common": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ng-web-apis/common/-/common-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-2Vnp4WTEqKZArhbKLgD1JIKjsDa3hWCa67OWaRWRH5sgX5xneVVaIAvC8gVpiCfl2p1Roen2kxfyYngx7G64SQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.2.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@angular/common": ">=12.0.0",
|
||||||
|
"@angular/core": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@ng-web-apis/mutation-observer": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ng-web-apis/mutation-observer/-/mutation-observer-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-f51Cu2DloNze1HaTWdUbtYFnt9VXhzpEnHDd9KFdiKOUNfEDx7wrSXIEQqv810hrq7F2jcIAERCdiqV6ItH7Pg==",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.2.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@angular/core": ">=12.0.0",
|
||||||
|
"@ng-web-apis/common": ">=2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@ng-web-apis/resize-observer": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ng-web-apis/resize-observer/-/resize-observer-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-umuXJepTYBCI3ZcW9873fozO0qt1PeHLBNM+wXA+7Wphy35+RQcPNmkwfgkKqWceIjlYAvyuPTNWa5TM1OEeqg==",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.2.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@angular/core": ">=12.0.0",
|
||||||
|
"@ng-web-apis/common": ">=2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@ngtools/webpack": {
|
"node_modules/@ngtools/webpack": {
|
||||||
"version": "14.2.3",
|
"version": "14.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-14.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-14.2.3.tgz",
|
||||||
@@ -16891,6 +16930,30 @@
|
|||||||
"tslib": "^2.0.0"
|
"tslib": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@ng-web-apis/common": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ng-web-apis/common/-/common-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-2Vnp4WTEqKZArhbKLgD1JIKjsDa3hWCa67OWaRWRH5sgX5xneVVaIAvC8gVpiCfl2p1Roen2kxfyYngx7G64SQ==",
|
||||||
|
"requires": {
|
||||||
|
"tslib": "^2.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@ng-web-apis/mutation-observer": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ng-web-apis/mutation-observer/-/mutation-observer-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-f51Cu2DloNze1HaTWdUbtYFnt9VXhzpEnHDd9KFdiKOUNfEDx7wrSXIEQqv810hrq7F2jcIAERCdiqV6ItH7Pg==",
|
||||||
|
"requires": {
|
||||||
|
"tslib": "^2.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@ng-web-apis/resize-observer": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ng-web-apis/resize-observer/-/resize-observer-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-umuXJepTYBCI3ZcW9873fozO0qt1PeHLBNM+wXA+7Wphy35+RQcPNmkwfgkKqWceIjlYAvyuPTNWa5TM1OEeqg==",
|
||||||
|
"requires": {
|
||||||
|
"tslib": "^2.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@ngtools/webpack": {
|
"@ngtools/webpack": {
|
||||||
"version": "14.2.3",
|
"version": "14.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-14.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-14.2.3.tgz",
|
||||||
|
|||||||
@@ -39,6 +39,9 @@
|
|||||||
"@angular/platform-browser-dynamic": "^14.1.0",
|
"@angular/platform-browser-dynamic": "^14.1.0",
|
||||||
"@angular/router": "^14.1.0",
|
"@angular/router": "^14.1.0",
|
||||||
"@ionic/angular": "^6.1.15",
|
"@ionic/angular": "^6.1.15",
|
||||||
|
"@ng-web-apis/common": "^2.0.0",
|
||||||
|
"@ng-web-apis/mutation-observer": "^2.0.0",
|
||||||
|
"@ng-web-apis/resize-observer": "^2.0.0",
|
||||||
"@materia-ui/ngx-monaco-editor": "^6.0.0",
|
"@materia-ui/ngx-monaco-editor": "^6.0.0",
|
||||||
"@start9labs/argon2": "^0.1.0",
|
"@start9labs/argon2": "^0.1.0",
|
||||||
"@start9labs/emver": "^0.1.5",
|
"@start9labs/emver": "^0.1.5",
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
"@angular/core": ">=13.2.0",
|
"@angular/core": ">=13.2.0",
|
||||||
"@angular/router": ">=13.2.0",
|
"@angular/router": ">=13.2.0",
|
||||||
"@ionic/angular": ">=6.0.0",
|
"@ionic/angular": ">=6.0.0",
|
||||||
|
"@ng-web-apis/mutation-observer": ">=2.0.0",
|
||||||
|
"@ng-web-apis/resize-observer": ">=2.0.0",
|
||||||
"@start9labs/emver": "^0.1.5"
|
"@start9labs/emver": "^0.1.5"
|
||||||
},
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<div (elasticContainer)="height = $event">
|
||||||
|
<ng-content></ng-content>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: height 0.25s;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component, HostBinding } from '@angular/core'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'elastic-container',
|
||||||
|
templateUrl: './elastic-container.component.html',
|
||||||
|
styleUrls: ['./elastic-container.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class ElasticContainerComponent {
|
||||||
|
@HostBinding('style.height.px')
|
||||||
|
height = NaN
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { Directive, ElementRef, inject, Output } from '@angular/core'
|
||||||
|
import { ResizeObserverService } from '@ng-web-apis/resize-observer'
|
||||||
|
import {
|
||||||
|
MUTATION_OBSERVER_INIT,
|
||||||
|
MutationObserverService,
|
||||||
|
} from '@ng-web-apis/mutation-observer'
|
||||||
|
import { distinctUntilChanged, map, merge } from 'rxjs'
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: '[elasticContainer]',
|
||||||
|
providers: [
|
||||||
|
ResizeObserverService,
|
||||||
|
MutationObserverService,
|
||||||
|
{
|
||||||
|
provide: MUTATION_OBSERVER_INIT,
|
||||||
|
useValue: {
|
||||||
|
childList: true,
|
||||||
|
characterData: true,
|
||||||
|
subtree: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class ElasticContainerDirective {
|
||||||
|
private readonly elementRef = inject(ElementRef)
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
readonly elasticContainer = merge(
|
||||||
|
inject(ResizeObserverService),
|
||||||
|
inject(MutationObserverService),
|
||||||
|
).pipe(
|
||||||
|
map(() => this.elementRef.nativeElement.clientHeight),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { NgModule } from '@angular/core'
|
||||||
|
|
||||||
|
import { ElasticContainerComponent } from './elastic-container.component'
|
||||||
|
import { ElasticContainerDirective } from './elastic-container.directive'
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [ElasticContainerComponent, ElasticContainerDirective],
|
||||||
|
exports: [ElasticContainerComponent],
|
||||||
|
})
|
||||||
|
export class ElasticContainerModule {}
|
||||||
@@ -9,6 +9,9 @@ export * from './components/alert/alert.component'
|
|||||||
export * from './components/alert/alert.module'
|
export * from './components/alert/alert.module'
|
||||||
export * from './components/alert/alert-button.directive'
|
export * from './components/alert/alert-button.directive'
|
||||||
export * from './components/alert/alert-input.directive'
|
export * from './components/alert/alert-input.directive'
|
||||||
|
export * from './components/elastic-container/elastic-container.component'
|
||||||
|
export * from './components/elastic-container/elastic-container.directive'
|
||||||
|
export * from './components/elastic-container/elastic-container.module'
|
||||||
export * from './components/markdown/markdown.component'
|
export * from './components/markdown/markdown.component'
|
||||||
export * from './components/markdown/markdown.component.module'
|
export * from './components/markdown/markdown.component.module'
|
||||||
export * from './components/text-spinner/text-spinner.component'
|
export * from './components/text-spinner/text-spinner.component'
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
<div [hidden]="!control.dirty && !control.touched" class="error-message">
|
|
||||||
<!-- primitive -->
|
|
||||||
<p *ngIf="control.hasError('required')">{{ spec.name }} is required</p>
|
|
||||||
|
|
||||||
<!-- string -->
|
|
||||||
<p *ngIf="control.hasError('pattern')">
|
|
||||||
{{ $any(spec)['pattern-description'] }}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<!-- number -->
|
|
||||||
<ng-container *ngIf="spec.type === 'number'">
|
|
||||||
<p *ngIf="control.hasError('numberNotInteger')">
|
|
||||||
{{ spec.name }} must be an integer
|
|
||||||
</p>
|
|
||||||
<p *ngIf="control.hasError('numberNotInRange')">
|
|
||||||
{{ control.errors?.['numberNotInRange']?.value }}
|
|
||||||
</p>
|
|
||||||
<p *ngIf="control.hasError('notNumber')">
|
|
||||||
{{ spec.name }} must be a number
|
|
||||||
</p>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<!-- list -->
|
|
||||||
<ng-container *ngIf="spec.type === 'list'">
|
|
||||||
<p *ngIf="control.hasError('listNotInRange')">
|
|
||||||
{{ control.errors?.['listNotInRange']?.value }}
|
|
||||||
</p>
|
|
||||||
<p *ngIf="control.hasError('listNotUnique')">
|
|
||||||
{{ control.errors?.['listNotUnique']?.value }}
|
|
||||||
</p>
|
|
||||||
<p *ngIf="control.hasError('listItemIssue')">
|
|
||||||
{{ control.errors?.['listItemIssue']?.value }}
|
|
||||||
</p>
|
|
||||||
</ng-container>
|
|
||||||
</div>
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<ion-button
|
<ion-button
|
||||||
*ngIf="data.spec.description"
|
*ngIf="data.description"
|
||||||
class="slot-start"
|
class="slot-start"
|
||||||
fill="clear"
|
fill="clear"
|
||||||
(click)="presentAlertDescription($event)"
|
(click)="presentAlertDescription($event)"
|
||||||
@@ -7,21 +7,10 @@
|
|||||||
<ion-icon name="help-circle-outline" slot="icon-only" size="small"></ion-icon>
|
<ion-icon name="help-circle-outline" slot="icon-only" size="small"></ion-icon>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
|
|
||||||
<span>{{ data.spec.name }}</span>
|
<span>{{ data.name }}</span>
|
||||||
|
|
||||||
<ion-text color="success" *ngIf="data.new"> (New)</ion-text>
|
<ion-text color="success" *ngIf="data.new"> (New)</ion-text>
|
||||||
<ion-text color="success" *ngIf="data.newOptions"> (New Options)</ion-text>
|
<ion-text color="success" *ngIf="data.newOptions"> (New Options)</ion-text>
|
||||||
<ion-text color="warning" *ngIf="data.edited"> (Edited)</ion-text>
|
<ion-text color="warning" *ngIf="data.edited"> (Edited)</ion-text>
|
||||||
|
|
||||||
<span
|
<span *ngIf="data.required"> * </span>
|
||||||
*ngIf="
|
|
||||||
(['string', 'number'] | includes: data.spec.type) &&
|
|
||||||
!$any(data.spec).nullable
|
|
||||||
"
|
|
||||||
>
|
|
||||||
*
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span *ngIf="data.spec.type === 'list' && Range.from(data.spec.range).min"
|
|
||||||
> *</span
|
|
||||||
>
|
|
||||||
|
|||||||
@@ -1,66 +1,21 @@
|
|||||||
<ion-item-group [formGroup]="formGroup">
|
<ion-item-group [formGroup]="formGroup">
|
||||||
<div *ngFor="let entry of formGroup.controls | keyvalue: asIsOrder">
|
<div *ngFor="let entry of formGroup.controls | keyvalue: asIsOrder">
|
||||||
<!-- union enum -->
|
<div *ngIf="objectSpec[entry.key] as spec">
|
||||||
<ng-container *ngIf="unionSpec && entry.key === unionSpec.tag.id">
|
|
||||||
<ion-item>
|
|
||||||
<ion-button
|
|
||||||
*ngIf="unionSpec.tag.description"
|
|
||||||
class="slot-start"
|
|
||||||
fill="clear"
|
|
||||||
size="small"
|
|
||||||
(click)="
|
|
||||||
presentUnionTagDescription(
|
|
||||||
$event,
|
|
||||||
unionSpec.tag.name,
|
|
||||||
unionSpec.tag.description
|
|
||||||
)
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<ion-icon name="help-circle-outline"></ion-icon>
|
|
||||||
</ion-button>
|
|
||||||
<ion-label>
|
|
||||||
<ion-text color="dark">
|
|
||||||
<b>{{ unionSpec.tag.name }}</b>
|
|
||||||
</ion-text>
|
|
||||||
</ion-label>
|
|
||||||
<!-- class enter-click disables the enter click on the modal behind the select -->
|
|
||||||
<ion-select
|
|
||||||
[interfaceOptions]="{
|
|
||||||
message: getWarningText(unionSpec.warning),
|
|
||||||
cssClass: 'enter-click'
|
|
||||||
}"
|
|
||||||
slot="end"
|
|
||||||
placeholder="Select"
|
|
||||||
[formControlName]="unionSpec.tag.id"
|
|
||||||
[selectedText]="unionSpec.tag['variant-names'][entry.value.value]"
|
|
||||||
(ionChange)="updateUnion($event)"
|
|
||||||
>
|
|
||||||
<ion-select-option
|
|
||||||
*ngFor="let option of Object.keys(unionSpec.variants)"
|
|
||||||
[value]="option"
|
|
||||||
>
|
|
||||||
{{ unionSpec.tag['variant-names'][option] }}
|
|
||||||
</ion-select-option>
|
|
||||||
</ion-select>
|
|
||||||
</ion-item>
|
|
||||||
</ng-container>
|
|
||||||
<div *ngIf="objectSpec[entry.key] as spec" [class.indent]="unionSpec">
|
|
||||||
<!-- string or number -->
|
<!-- string or number -->
|
||||||
<ng-container *ngIf="spec.type === 'string' || spec.type === 'number'">
|
<ng-container *ngIf="spec.type === 'string' || spec.type === 'number'">
|
||||||
<!-- label -->
|
<!-- label -->
|
||||||
<h4
|
<h4 class="input-label">
|
||||||
class="input-label"
|
|
||||||
[class.validation-error]="formGroup.get(entry.key)?.errors"
|
|
||||||
>
|
|
||||||
<form-label
|
<form-label
|
||||||
[data]="{
|
[data]="{
|
||||||
spec: spec,
|
name: spec.name,
|
||||||
|
description: spec.description,
|
||||||
new: original?.[entry.key] === undefined,
|
new: original?.[entry.key] === undefined,
|
||||||
edited: entry.value.dirty
|
edited: entry.value.dirty,
|
||||||
|
required: !spec.nullable
|
||||||
}"
|
}"
|
||||||
></form-label>
|
></form-label>
|
||||||
</h4>
|
</h4>
|
||||||
<ion-item color="dark" class="ion-margin-bottom">
|
<ion-item color="dark">
|
||||||
<ion-textarea
|
<ion-textarea
|
||||||
*ngIf="spec.type === 'string' && spec.textarea; else notTextArea"
|
*ngIf="spec.type === 'string' && spec.textarea; else notTextArea"
|
||||||
[placeholder]="spec.placeholder || 'Enter ' + spec.name"
|
[placeholder]="spec.placeholder || 'Enter ' + spec.name"
|
||||||
@@ -107,22 +62,21 @@
|
|||||||
>{{ spec.units }}</ion-note
|
>{{ spec.units }}</ion-note
|
||||||
>
|
>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<form-error
|
<p class="error-message">
|
||||||
*ngIf="formGroup.get(entry.key)?.errors"
|
<span *ngIf="(formGroup | getControl: entry.key).errors as errors">
|
||||||
[control]="$any(formGroup.get(entry.key))"
|
{{ errors | getError: $any(spec)['pattern-description'] }}
|
||||||
[spec]="spec"
|
</span>
|
||||||
>
|
</p>
|
||||||
</form-error>
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<!-- boolean or enum -->
|
<!-- boolean or enum -->
|
||||||
<ion-item
|
<ion-item
|
||||||
*ngIf="['boolean', 'enum'] | includes: spec.type"
|
*ngIf="spec.type === 'boolean' || spec.type === 'enum'"
|
||||||
style="--padding-start: 0"
|
style="--padding-start: 0"
|
||||||
>
|
>
|
||||||
<ion-button
|
<ion-button
|
||||||
*ngIf="spec.description"
|
*ngIf="spec.description"
|
||||||
fill="clear"
|
fill="clear"
|
||||||
(click)="presentAlertDescription($event, spec)"
|
(click)="presentAlertBoolEnumDescription($event, spec)"
|
||||||
style="--padding-start: 0"
|
style="--padding-start: 0"
|
||||||
>
|
>
|
||||||
<ion-icon
|
<ion-icon
|
||||||
@@ -157,7 +111,7 @@
|
|||||||
<ion-select
|
<ion-select
|
||||||
*ngIf="spec.type === 'enum' && formGroup.get(entry.key) as control"
|
*ngIf="spec.type === 'enum' && formGroup.get(entry.key) as control"
|
||||||
[interfaceOptions]="{
|
[interfaceOptions]="{
|
||||||
message: getWarningText(spec.warning),
|
message: spec.warning | toWarningText,
|
||||||
cssClass: 'enter-click'
|
cssClass: 'enter-click'
|
||||||
}"
|
}"
|
||||||
slot="end"
|
slot="end"
|
||||||
@@ -173,21 +127,21 @@
|
|||||||
</ion-select-option>
|
</ion-select-option>
|
||||||
</ion-select>
|
</ion-select>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<!-- object or union -->
|
<!-- object -->
|
||||||
<ng-container *ngIf="spec.type === 'object' || spec.type === 'union'">
|
<ng-container *ngIf="spec.type === 'object'">
|
||||||
<!-- label -->
|
<!-- label -->
|
||||||
<ion-item-divider
|
<ion-item-divider
|
||||||
(click)="toggleExpandObject(entry.key)"
|
(click)="toggleExpandObject(entry.key)"
|
||||||
style="cursor: pointer"
|
style="cursor: pointer"
|
||||||
[class.error-border]="entry.value.invalid"
|
[class.error-border]="entry.value.invalid"
|
||||||
[class.validation-error]="entry.value.invalid"
|
|
||||||
>
|
>
|
||||||
<form-label
|
<form-label
|
||||||
[data]="{
|
[data]="{
|
||||||
spec: spec,
|
name: spec.name,
|
||||||
|
description: spec.description,
|
||||||
new: original?.[entry.key] === undefined,
|
new: original?.[entry.key] === undefined,
|
||||||
newOptions: objectDisplay[entry.key].hasNewOptions,
|
edited: entry.value.dirty,
|
||||||
edited: entry.value.dirty
|
newOptions: objectDisplay[entry.key].hasNewOptions
|
||||||
}"
|
}"
|
||||||
></form-label>
|
></form-label>
|
||||||
<ion-icon
|
<ion-icon
|
||||||
@@ -198,45 +152,40 @@
|
|||||||
transform: objectDisplay[entry.key].expanded
|
transform: objectDisplay[entry.key].expanded
|
||||||
? 'rotate(0deg)'
|
? 'rotate(0deg)'
|
||||||
: 'rotate(180deg)',
|
: 'rotate(180deg)',
|
||||||
transition: 'transform 0.25s ease-out'
|
transition: 'transform 0.42s ease-out'
|
||||||
}"
|
}"
|
||||||
></ion-icon>
|
></ion-icon>
|
||||||
</ion-item-divider>
|
</ion-item-divider>
|
||||||
<!-- body -->
|
<!-- body -->
|
||||||
<div
|
<div
|
||||||
[id]="getElementId(entry.key)"
|
[id]="objectId | toElementId: entry.key"
|
||||||
[ngStyle]="{
|
[ngStyle]="{
|
||||||
'max-height': objectDisplay[entry.key].height,
|
'max-height': objectDisplay[entry.key].height,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
'transition-property': 'max-height',
|
'transition-property': 'max-height',
|
||||||
'transition-duration': '.25s'
|
'transition-duration': '.42s'
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<div class="nested-wrapper">
|
<div class="nested-wrapper">
|
||||||
<form-object
|
<form-object
|
||||||
*ngIf="spec.type === 'object'"
|
|
||||||
[objectSpec]="spec.spec"
|
[objectSpec]="spec.spec"
|
||||||
[formGroup]="$any(entry.value)"
|
[formGroup]="$any(entry.value)"
|
||||||
[current]="current?.[entry.key]"
|
[current]="current?.[entry.key]"
|
||||||
[original]="original?.[entry.key]"
|
[original]="original?.[entry.key]"
|
||||||
(onExpand)="resize(entry.key)"
|
(onResize)="resize(entry.key)"
|
||||||
(hasNewOptions)="setHasNew(entry.key)"
|
|
||||||
></form-object>
|
|
||||||
<form-object
|
|
||||||
*ngIf="spec.type === 'union'"
|
|
||||||
[objectSpec]="
|
|
||||||
spec.variants[$any(entry.value).controls[spec.tag.id].value]
|
|
||||||
"
|
|
||||||
[formGroup]="$any(entry.value)"
|
|
||||||
[current]="current?.[entry.key]"
|
|
||||||
[original]="original?.[entry.key][spec.tag.id] === current?.[entry.key][spec.tag.id] ? original?.[entry.key] : undefined"
|
|
||||||
[unionSpec]="spec"
|
|
||||||
(onExpand)="resize(entry.key)"
|
|
||||||
(hasNewOptions)="setHasNew(entry.key)"
|
(hasNewOptions)="setHasNew(entry.key)"
|
||||||
></form-object>
|
></form-object>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
<!-- union -->
|
||||||
|
<form-union
|
||||||
|
*ngIf="spec.type === 'union'"
|
||||||
|
[spec]="spec"
|
||||||
|
[formGroup]="$any(entry.value)"
|
||||||
|
[current]="current?.[entry.key]"
|
||||||
|
[original]="original?.[entry.key]"
|
||||||
|
></form-union>
|
||||||
<!-- list (not enum) -->
|
<!-- list (not enum) -->
|
||||||
<ng-container *ngIf="spec.type === 'list' && spec.subtype !== 'enum'">
|
<ng-container *ngIf="spec.type === 'list' && spec.subtype !== 'enum'">
|
||||||
<ng-container
|
<ng-container
|
||||||
@@ -244,15 +193,14 @@
|
|||||||
[formArrayName]="entry.key"
|
[formArrayName]="entry.key"
|
||||||
>
|
>
|
||||||
<!-- label -->
|
<!-- label -->
|
||||||
<ion-item-divider
|
<ion-item-divider [class.error-border]="entry.value.invalid">
|
||||||
[class.error-border]="entry.value.invalid"
|
|
||||||
[class.validation-error]="entry.value.invalid"
|
|
||||||
>
|
|
||||||
<form-label
|
<form-label
|
||||||
[data]="{
|
[data]="{
|
||||||
spec: spec,
|
name: spec.name,
|
||||||
|
description: spec.description,
|
||||||
new: original?.[entry.key] === undefined,
|
new: original?.[entry.key] === undefined,
|
||||||
edited: entry.value.dirty
|
edited: entry.value.dirty,
|
||||||
|
required: !!(spec.range | toRange).min
|
||||||
}"
|
}"
|
||||||
></form-label>
|
></form-label>
|
||||||
<ion-button
|
<ion-button
|
||||||
@@ -266,12 +214,11 @@
|
|||||||
Add
|
Add
|
||||||
</ion-button>
|
</ion-button>
|
||||||
</ion-item-divider>
|
</ion-item-divider>
|
||||||
<form-error
|
<p class="error-message" style="margin-bottom: 8px">
|
||||||
*ngIf="formGroup.get(entry.key)?.errors"
|
<span *ngIf="(formGroup | getControl: entry.key).errors as errors">
|
||||||
[control]="$any(formGroup.get(entry.key))"
|
{{ errors | getError }}
|
||||||
[spec]="spec"
|
</span>
|
||||||
>
|
</p>
|
||||||
</form-error>
|
|
||||||
<!-- body -->
|
<!-- body -->
|
||||||
<div class="nested-wrapper">
|
<div class="nested-wrapper">
|
||||||
<div
|
<div
|
||||||
@@ -279,13 +226,12 @@
|
|||||||
let abstractControl of $any(formArr).controls;
|
let abstractControl of $any(formArr).controls;
|
||||||
let i = index
|
let i = index
|
||||||
"
|
"
|
||||||
class="ion-padding-top"
|
|
||||||
>
|
>
|
||||||
<!-- nested -->
|
<!-- object or union -->
|
||||||
<ng-container
|
<ng-container
|
||||||
*ngIf="spec.subtype === 'object' || spec.subtype === 'union'"
|
*ngIf="spec.subtype === 'object' || spec.subtype === 'union'"
|
||||||
>
|
>
|
||||||
<!-- nested label -->
|
<!-- object/union label -->
|
||||||
<ion-item
|
<ion-item
|
||||||
button
|
button
|
||||||
(click)="toggleExpandListObject(entry.key, i)"
|
(click)="toggleExpandListObject(entry.key, i)"
|
||||||
@@ -293,11 +239,9 @@
|
|||||||
>
|
>
|
||||||
<form-label
|
<form-label
|
||||||
[data]="{
|
[data]="{
|
||||||
spec: $any({
|
name:
|
||||||
name:
|
objectListDisplay[entry.key][i].displayAs ||
|
||||||
objectListDisplay[entry.key][i].displayAs ||
|
'Entry ' + (i + 1),
|
||||||
'Entry ' + (i + 1)
|
|
||||||
}),
|
|
||||||
new: false,
|
new: false,
|
||||||
edited: abstractControl.dirty
|
edited: abstractControl.dirty
|
||||||
}"
|
}"
|
||||||
@@ -310,41 +254,43 @@
|
|||||||
transform: objectListDisplay[entry.key][i].expanded
|
transform: objectListDisplay[entry.key][i].expanded
|
||||||
? 'rotate(0deg)'
|
? 'rotate(0deg)'
|
||||||
: 'rotate(180deg)',
|
: 'rotate(180deg)',
|
||||||
transition: 'transform 0.25s ease-out'
|
transition: 'transform 0.42s ease-out'
|
||||||
}"
|
}"
|
||||||
></ion-icon>
|
></ion-icon>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<!-- nested body -->
|
<!-- object/union body -->
|
||||||
<div
|
<div
|
||||||
style="padding-left: 24px"
|
style="padding-left: 24px"
|
||||||
[id]="getElementId(entry.key, i)"
|
[id]="objectId | toElementId: entry.key:i"
|
||||||
[ngStyle]="{
|
[ngStyle]="{
|
||||||
'max-height': objectListDisplay[entry.key][i].height,
|
'max-height': objectListDisplay[entry.key][i].height,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
'transition-property': 'max-height',
|
'transition-property': 'max-height',
|
||||||
'transition-duration': '.25s'
|
'transition-duration': '.42s'
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<form-object
|
<form-object
|
||||||
[objectSpec]="
|
*ngIf="spec.subtype === 'object'"
|
||||||
spec.subtype === 'union'
|
[objectSpec]="$any(spec.spec).spec"
|
||||||
? $any(spec.spec).variants[
|
|
||||||
abstractControl.controls[$any(spec.spec).tag.id]
|
|
||||||
.value
|
|
||||||
]
|
|
||||||
: $any(spec.spec).spec
|
|
||||||
"
|
|
||||||
[formGroup]="abstractControl"
|
[formGroup]="abstractControl"
|
||||||
[current]="current?.[entry.key]?.[i]"
|
[current]="current?.[entry.key]?.[i]"
|
||||||
[original]="original?.[entry.key]?.[i]"
|
[original]="original?.[entry.key]?.[i]"
|
||||||
[unionSpec]="
|
|
||||||
spec.subtype === 'union' ? $any(spec.spec) : undefined
|
|
||||||
"
|
|
||||||
(onInputChange)="
|
(onInputChange)="
|
||||||
updateLabel(entry.key, i, $any(spec.spec)['display-as'])
|
updateLabel(entry.key, i, $any(spec.spec)['display-as'])
|
||||||
"
|
"
|
||||||
(onExpand)="resize(entry.key, i)"
|
(onResize)="resize(entry.key, i)"
|
||||||
></form-object>
|
></form-object>
|
||||||
|
<form-union
|
||||||
|
*ngIf="spec.subtype === 'union'"
|
||||||
|
[spec]="$any(spec.spec)"
|
||||||
|
[formGroup]="abstractControl"
|
||||||
|
[current]="current?.[entry.key]?.[i]"
|
||||||
|
[original]="original?.[entry.key]?.[i]"
|
||||||
|
(onInputChange)="
|
||||||
|
updateLabel(entry.key, i, $any(spec.spec)['display-as'])
|
||||||
|
"
|
||||||
|
(onResize)="resize(entry.key, i)"
|
||||||
|
></form-union>
|
||||||
<div style="text-align: right; padding-top: 12px">
|
<div style="text-align: right; padding-top: 12px">
|
||||||
<ion-button
|
<ion-button
|
||||||
fill="clear"
|
fill="clear"
|
||||||
@@ -358,9 +304,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<!-- string or number -->
|
<!-- string or number -->
|
||||||
<ion-item-group
|
<div
|
||||||
[id]="getElementId(entry.key, i)"
|
|
||||||
*ngIf="spec.subtype === 'string' || spec.subtype === 'number'"
|
*ngIf="spec.subtype === 'string' || spec.subtype === 'number'"
|
||||||
|
[id]="objectId | toElementId: entry.key:i"
|
||||||
>
|
>
|
||||||
<ion-item color="dark">
|
<ion-item color="dark">
|
||||||
<ion-input
|
<ion-input
|
||||||
@@ -382,13 +328,16 @@
|
|||||||
<ion-icon slot="icon-only" name="close"></ion-icon>
|
<ion-icon slot="icon-only" name="close"></ion-icon>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<form-error
|
<p class="error-message">
|
||||||
*ngIf="abstractControl.errors"
|
<span
|
||||||
[control]="abstractControl"
|
*ngIf="
|
||||||
[spec]="$any(spec.spec)"
|
(formGroup | getControl: entry.key:i).errors as errors
|
||||||
>
|
"
|
||||||
</form-error>
|
>
|
||||||
</ion-item-group>
|
{{ errors | getError: $any(spec)['pattern-description'] }}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
@@ -402,9 +351,9 @@
|
|||||||
<!-- label -->
|
<!-- label -->
|
||||||
<p class="input-label">
|
<p class="input-label">
|
||||||
<form-label
|
<form-label
|
||||||
[class.validation-error]="entry.value.invalid"
|
|
||||||
[data]="{
|
[data]="{
|
||||||
spec: spec,
|
name: spec.name,
|
||||||
|
description: spec.description,
|
||||||
new: original?.[entry.key] === undefined,
|
new: original?.[entry.key] === undefined,
|
||||||
edited: entry.value.dirty
|
edited: entry.value.dirty
|
||||||
}"
|
}"
|
||||||
@@ -418,18 +367,17 @@
|
|||||||
(click)="presentModalEnumList(entry.key, $any(spec), formArr.value)"
|
(click)="presentModalEnumList(entry.key, $any(spec), formArr.value)"
|
||||||
>
|
>
|
||||||
<ion-label style="white-space: nowrap !important">
|
<ion-label style="white-space: nowrap !important">
|
||||||
<h2>{{ getEnumListDisplay(formArr.value, $any(spec.spec)) }}</h2>
|
<h2>{{ formArr.value | toEnumListDisplay: $any(spec.spec) }}</h2>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
<ion-button slot="end" fill="clear" color="light">
|
<ion-button slot="end" fill="clear" color="light">
|
||||||
<ion-icon slot="icon-only" name="chevron-down"></ion-icon>
|
<ion-icon slot="icon-only" name="chevron-down"></ion-icon>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<form-error
|
<p class="error-message">
|
||||||
*ngIf="formGroup.get(entry.key)?.errors"
|
<span *ngIf="(formGroup | getControl: entry.key).errors as errors">
|
||||||
[control]="$any(formGroup.get(entry.key))"
|
{{ errors | getError }}
|
||||||
[spec]="spec"
|
</span>
|
||||||
>
|
</p>
|
||||||
</form-error>
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,16 +2,34 @@ import { NgModule } from '@angular/core'
|
|||||||
import { CommonModule } from '@angular/common'
|
import { CommonModule } from '@angular/common'
|
||||||
import {
|
import {
|
||||||
FormObjectComponent,
|
FormObjectComponent,
|
||||||
|
FormUnionComponent,
|
||||||
FormLabelComponent,
|
FormLabelComponent,
|
||||||
FormErrorComponent,
|
|
||||||
} from './form-object.component'
|
} from './form-object.component'
|
||||||
|
import {
|
||||||
|
GetErrorPipe,
|
||||||
|
ToWarningTextPipe,
|
||||||
|
ToElementIdPipe,
|
||||||
|
GetControlPipe,
|
||||||
|
ToEnumListDisplayPipe,
|
||||||
|
ToRangePipe,
|
||||||
|
} from './form-object.pipes'
|
||||||
import { IonicModule } from '@ionic/angular'
|
import { IonicModule } from '@ionic/angular'
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { SharedPipesModule } from '@start9labs/shared'
|
import { ElasticContainerModule, SharedPipesModule } from '@start9labs/shared'
|
||||||
import { EnumListPageModule } from 'src/app/modals/enum-list/enum-list.module'
|
import { EnumListPageModule } from 'src/app/modals/enum-list/enum-list.module'
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [FormObjectComponent, FormLabelComponent, FormErrorComponent],
|
declarations: [
|
||||||
|
FormObjectComponent,
|
||||||
|
FormUnionComponent,
|
||||||
|
FormLabelComponent,
|
||||||
|
ToWarningTextPipe,
|
||||||
|
GetErrorPipe,
|
||||||
|
ToEnumListDisplayPipe,
|
||||||
|
ToElementIdPipe,
|
||||||
|
GetControlPipe,
|
||||||
|
ToRangePipe,
|
||||||
|
],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
IonicModule,
|
IonicModule,
|
||||||
@@ -19,7 +37,8 @@ import { EnumListPageModule } from 'src/app/modals/enum-list/enum-list.module'
|
|||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
SharedPipesModule,
|
SharedPipesModule,
|
||||||
EnumListPageModule,
|
EnumListPageModule,
|
||||||
|
ElasticContainerModule,
|
||||||
],
|
],
|
||||||
exports: [FormObjectComponent, FormLabelComponent, FormErrorComponent],
|
exports: [FormObjectComponent, FormLabelComponent],
|
||||||
})
|
})
|
||||||
export class FormObjectComponentModule {}
|
export class FormObjectComponentModule {}
|
||||||
|
|||||||
@@ -31,16 +31,10 @@ ion-item-divider {
|
|||||||
padding: 0 0 16px 24px;
|
padding: 0 0 16px 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.validation-error {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-message {
|
.error-message {
|
||||||
p {
|
margin-top: 2px;
|
||||||
margin-bottom: 4px;
|
font-size: small;
|
||||||
font-size: small;
|
color: var(--ion-color-danger);
|
||||||
color: var(--ion-color-danger);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.indent {
|
.indent {
|
||||||
|
|||||||
@@ -1,30 +1,28 @@
|
|||||||
import { Component, Input, Output, EventEmitter } from '@angular/core'
|
|
||||||
import {
|
import {
|
||||||
AbstractFormGroupDirective,
|
Component,
|
||||||
FormArray,
|
Input,
|
||||||
UntypedFormArray,
|
Output,
|
||||||
UntypedFormGroup,
|
EventEmitter,
|
||||||
} from '@angular/forms'
|
ChangeDetectionStrategy,
|
||||||
import {
|
Inject,
|
||||||
AlertButton,
|
} from '@angular/core'
|
||||||
AlertController,
|
import { FormArray, UntypedFormArray, UntypedFormGroup } from '@angular/forms'
|
||||||
IonicSafeString,
|
import { AlertButton, AlertController, ModalController } from '@ionic/angular'
|
||||||
ModalController,
|
|
||||||
} from '@ionic/angular'
|
|
||||||
import {
|
import {
|
||||||
ConfigSpec,
|
ConfigSpec,
|
||||||
ListValueSpecOf,
|
ListValueSpecOf,
|
||||||
ValueSpec,
|
ValueSpec,
|
||||||
ValueSpecBoolean,
|
ValueSpecBoolean,
|
||||||
|
ValueSpecEnum,
|
||||||
ValueSpecList,
|
ValueSpecList,
|
||||||
ValueSpecListOf,
|
ValueSpecListOf,
|
||||||
ValueSpecUnion,
|
ValueSpecUnion,
|
||||||
} from 'src/app/pkg-config/config-types'
|
} from 'src/app/pkg-config/config-types'
|
||||||
import { FormService } from 'src/app/services/form.service'
|
import { FormService } from 'src/app/services/form.service'
|
||||||
import { Range } from 'src/app/pkg-config/config-utilities'
|
|
||||||
import { EnumListPage } from 'src/app/modals/enum-list/enum-list.page'
|
import { EnumListPage } from 'src/app/modals/enum-list/enum-list.page'
|
||||||
import { pauseFor } from '@start9labs/shared'
|
import { pauseFor } from '@start9labs/shared'
|
||||||
import { v4 } from 'uuid'
|
import { v4 } from 'uuid'
|
||||||
|
import { DOCUMENT } from '@angular/common'
|
||||||
const Mustache = require('mustache')
|
const Mustache = require('mustache')
|
||||||
|
|
||||||
interface Config {
|
interface Config {
|
||||||
@@ -38,11 +36,10 @@ interface Config {
|
|||||||
export class FormObjectComponent {
|
export class FormObjectComponent {
|
||||||
@Input() objectSpec!: ConfigSpec
|
@Input() objectSpec!: ConfigSpec
|
||||||
@Input() formGroup!: UntypedFormGroup
|
@Input() formGroup!: UntypedFormGroup
|
||||||
@Input() unionSpec?: ValueSpecUnion
|
|
||||||
@Input() current?: Config
|
@Input() current?: Config
|
||||||
@Input() original?: Config
|
@Input() original?: Config
|
||||||
@Output() onInputChange = new EventEmitter<void>()
|
@Output() onInputChange = new EventEmitter<void>()
|
||||||
@Output() onExpand = new EventEmitter<void>()
|
@Output() onResize = new EventEmitter<void>()
|
||||||
@Output() hasNewOptions = new EventEmitter<void>()
|
@Output() hasNewOptions = new EventEmitter<void>()
|
||||||
warningAck: { [key: string]: boolean } = {}
|
warningAck: { [key: string]: boolean } = {}
|
||||||
unmasked: { [key: string]: boolean } = {}
|
unmasked: { [key: string]: boolean } = {}
|
||||||
@@ -52,14 +49,13 @@ export class FormObjectComponent {
|
|||||||
objectListDisplay: {
|
objectListDisplay: {
|
||||||
[key: string]: { expanded: boolean; height: string; displayAs: string }[]
|
[key: string]: { expanded: boolean; height: string; displayAs: string }[]
|
||||||
} = {}
|
} = {}
|
||||||
private objectId = v4()
|
objectId = v4()
|
||||||
|
|
||||||
Object = Object
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly alertCtrl: AlertController,
|
private readonly alertCtrl: AlertController,
|
||||||
private readonly modalCtrl: ModalController,
|
private readonly modalCtrl: ModalController,
|
||||||
private readonly formService: FormService,
|
private readonly formService: FormService,
|
||||||
|
@Inject(DOCUMENT) private readonly document: Document,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
@@ -80,7 +76,7 @@ export class FormObjectComponent {
|
|||||||
: '',
|
: '',
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else if (['object', 'union'].includes(spec.type)) {
|
} else if (spec.type === 'object') {
|
||||||
this.objectDisplay[key] = {
|
this.objectDisplay[key] = {
|
||||||
expanded: false,
|
expanded: false,
|
||||||
height: '0px',
|
height: '0px',
|
||||||
@@ -101,64 +97,30 @@ export class FormObjectComponent {
|
|||||||
}, 10)
|
}, 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
getEnumListDisplay(arr: string[], spec: ListValueSpecOf<'enum'>): string {
|
|
||||||
return arr.map((v: string) => spec['value-names'][v]).join(', ')
|
|
||||||
}
|
|
||||||
|
|
||||||
updateUnion(e: any): void {
|
|
||||||
const id = this.unionSpec?.tag.id
|
|
||||||
|
|
||||||
Object.keys(this.formGroup.controls).forEach(control => {
|
|
||||||
if (control === id) return
|
|
||||||
this.formGroup.removeControl(control)
|
|
||||||
})
|
|
||||||
|
|
||||||
const unionGroup = this.formService.getUnionObject(
|
|
||||||
this.unionSpec as ValueSpecUnion,
|
|
||||||
e.detail.value,
|
|
||||||
)
|
|
||||||
|
|
||||||
Object.keys(unionGroup.controls).forEach(control => {
|
|
||||||
if (control === id) return
|
|
||||||
this.formGroup.addControl(control, unionGroup.controls[control])
|
|
||||||
})
|
|
||||||
|
|
||||||
Object.entries(this.unionSpec?.variants[e.detail.value] || {}).forEach(
|
|
||||||
([key, value]) => {
|
|
||||||
if (['object', 'union'].includes(value.type)) {
|
|
||||||
this.objectDisplay[key] = {
|
|
||||||
expanded: false,
|
|
||||||
height: '0px',
|
|
||||||
hasNewOptions: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
this.onExpand.emit()
|
|
||||||
}
|
|
||||||
|
|
||||||
resize(key: string, i?: number): void {
|
resize(key: string, i?: number): void {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (i !== undefined) {
|
if (i !== undefined) {
|
||||||
this.objectListDisplay[key][i].height = this.getDocSize(key, i)
|
this.objectListDisplay[key][i].height = this.getScrollHeight(key, i)
|
||||||
} else {
|
} else {
|
||||||
this.objectDisplay[key].height = this.getDocSize(key)
|
this.objectDisplay[key].height = this.getScrollHeight(key)
|
||||||
}
|
}
|
||||||
this.onExpand.emit()
|
this.onResize.emit()
|
||||||
}, 250) // 250 to match transition-duration, defined in html
|
}, 250) // 250 to match transition-duration defined in html, for smooth recursive resize
|
||||||
}
|
}
|
||||||
|
|
||||||
addListItemWrapper(key: string, spec: ValueSpec) {
|
addListItemWrapper<T extends ValueSpec>(
|
||||||
|
key: string,
|
||||||
|
spec: T extends ValueSpecUnion ? never : T,
|
||||||
|
) {
|
||||||
this.presentAlertChangeWarning(key, spec, () => this.addListItem(key))
|
this.presentAlertChangeWarning(key, spec, () => this.addListItem(key))
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleExpandObject(key: string) {
|
toggleExpandObject(key: string) {
|
||||||
this.objectDisplay[key].expanded = !this.objectDisplay[key].expanded
|
this.objectDisplay[key].expanded = !this.objectDisplay[key].expanded
|
||||||
this.objectDisplay[key].height = this.objectDisplay[key].expanded
|
this.objectDisplay[key].height = this.objectDisplay[key].expanded
|
||||||
? this.getDocSize(key)
|
? this.getScrollHeight(key)
|
||||||
: '0px'
|
: '0px'
|
||||||
this.onExpand.emit()
|
this.onResize.emit()
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleExpandListObject(key: string, i: number) {
|
toggleExpandListObject(key: string, i: number) {
|
||||||
@@ -166,9 +128,9 @@ export class FormObjectComponent {
|
|||||||
!this.objectListDisplay[key][i].expanded
|
!this.objectListDisplay[key][i].expanded
|
||||||
this.objectListDisplay[key][i].height = this.objectListDisplay[key][i]
|
this.objectListDisplay[key][i].height = this.objectListDisplay[key][i]
|
||||||
.expanded
|
.expanded
|
||||||
? this.getDocSize(key, i)
|
? this.getScrollHeight(key, i)
|
||||||
: '0px'
|
: '0px'
|
||||||
this.onExpand.emit()
|
this.onResize.emit()
|
||||||
}
|
}
|
||||||
|
|
||||||
updateLabel(key: string, i: number, displayAs: string) {
|
updateLabel(key: string, i: number, displayAs: string) {
|
||||||
@@ -177,12 +139,6 @@ export class FormObjectComponent {
|
|||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
|
|
||||||
getWarningText(text: string = ''): IonicSafeString | string {
|
|
||||||
return text
|
|
||||||
? new IonicSafeString(`<ion-text color="warning">${text}</ion-text>`)
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
|
|
||||||
handleInputChange() {
|
handleInputChange() {
|
||||||
this.onInputChange.emit()
|
this.onInputChange.emit()
|
||||||
}
|
}
|
||||||
@@ -224,9 +180,9 @@ export class FormObjectComponent {
|
|||||||
await modal.present()
|
await modal.present()
|
||||||
}
|
}
|
||||||
|
|
||||||
async presentAlertChangeWarning(
|
async presentAlertChangeWarning<T extends ValueSpec>(
|
||||||
key: string,
|
key: string,
|
||||||
spec: ValueSpec,
|
spec: T extends ValueSpecUnion ? never : T,
|
||||||
okFn?: Function,
|
okFn?: Function,
|
||||||
cancelFn?: Function,
|
cancelFn?: Function,
|
||||||
) {
|
) {
|
||||||
@@ -282,7 +238,10 @@ export class FormObjectComponent {
|
|||||||
await alert.present()
|
await alert.present()
|
||||||
}
|
}
|
||||||
|
|
||||||
async presentAlertDescription(event: Event, spec: ValueSpec) {
|
async presentAlertBoolEnumDescription(
|
||||||
|
event: Event,
|
||||||
|
spec: ValueSpecBoolean | ValueSpecEnum,
|
||||||
|
) {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
const { name, description } = spec
|
const { name, description } = spec
|
||||||
|
|
||||||
@@ -318,15 +277,18 @@ export class FormObjectComponent {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
this.onExpand.emit()
|
setTimeout(() => {
|
||||||
|
const element = this.document.getElementById(
|
||||||
pauseFor(400).then(() => {
|
getElementId(this.objectId, key, index),
|
||||||
const element = document.getElementById(this.getElementId(key, index))
|
)
|
||||||
element?.parentElement?.scrollIntoView({ behavior: 'smooth' })
|
element?.parentElement?.scrollIntoView({ behavior: 'smooth' })
|
||||||
})
|
|
||||||
|
if (['object', 'union'].includes(listSpec.subtype)) {
|
||||||
|
pauseFor(250).then(() => this.toggleExpandListObject(key, index))
|
||||||
|
}
|
||||||
|
}, 100)
|
||||||
|
|
||||||
arr.markAsDirty()
|
arr.markAsDirty()
|
||||||
newItem.markAllAsTouched()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private deleteListItem(key: string, index: number, markDirty = true): void {
|
private deleteListItem(key: string, index: number, markDirty = true): void {
|
||||||
@@ -360,57 +322,106 @@ export class FormObjectComponent {
|
|||||||
})
|
})
|
||||||
|
|
||||||
arr.markAsDirty()
|
arr.markAsDirty()
|
||||||
arr.markAllAsTouched()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getDocSize(key: string, index = 0): string {
|
private getScrollHeight(key: string, index = 0): string {
|
||||||
const element = document.getElementById(this.getElementId(key, index))
|
const element = this.document.getElementById(
|
||||||
|
getElementId(this.objectId, key, index),
|
||||||
|
)
|
||||||
return `${element?.scrollHeight}px`
|
return `${element?.scrollHeight}px`
|
||||||
}
|
}
|
||||||
|
|
||||||
getElementId(key: string, index = 0): string {
|
|
||||||
return `${key}-${index}-${this.objectId}`
|
|
||||||
}
|
|
||||||
|
|
||||||
async presentUnionTagDescription(
|
|
||||||
event: Event,
|
|
||||||
name: string,
|
|
||||||
description: string,
|
|
||||||
) {
|
|
||||||
event.stopPropagation()
|
|
||||||
const alert = await this.alertCtrl.create({
|
|
||||||
header: name,
|
|
||||||
message: description,
|
|
||||||
})
|
|
||||||
await alert.present()
|
|
||||||
}
|
|
||||||
|
|
||||||
asIsOrder() {
|
asIsOrder() {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface HeaderData {
|
@Component({
|
||||||
spec: ValueSpec
|
selector: 'form-union',
|
||||||
edited: boolean
|
templateUrl: './form-union.component.html',
|
||||||
new: boolean
|
styleUrls: ['./form-object.component.scss'],
|
||||||
newOptions?: boolean
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class FormUnionComponent {
|
||||||
|
@Input() formGroup!: UntypedFormGroup
|
||||||
|
@Input() spec!: ValueSpecUnion
|
||||||
|
@Input() current?: Config
|
||||||
|
@Input() original?: Config
|
||||||
|
|
||||||
|
@Output() onResize = new EventEmitter<void>()
|
||||||
|
|
||||||
|
get unionValue() {
|
||||||
|
return this.formGroup.get(this.spec.tag.id)?.value
|
||||||
|
}
|
||||||
|
|
||||||
|
get isNew() {
|
||||||
|
return !this.original
|
||||||
|
}
|
||||||
|
|
||||||
|
get hasNewOptions() {
|
||||||
|
const tagId = this.spec.tag.id
|
||||||
|
return (
|
||||||
|
this.original?.[tagId] === this.current?.[tagId] &&
|
||||||
|
!!Object.keys(this.current || {}).find(
|
||||||
|
key => this.original![key] === undefined,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
objectId = v4()
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly formService: FormService,
|
||||||
|
@Inject(DOCUMENT) private readonly document: Document,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
updateUnion(e: any): void {
|
||||||
|
const tagId = this.spec.tag.id
|
||||||
|
|
||||||
|
Object.keys(this.formGroup.controls).forEach(control => {
|
||||||
|
if (control === tagId) return
|
||||||
|
this.formGroup.removeControl(control)
|
||||||
|
})
|
||||||
|
|
||||||
|
const unionGroup = this.formService.getUnionObject(
|
||||||
|
this.spec as ValueSpecUnion,
|
||||||
|
e.detail.value,
|
||||||
|
)
|
||||||
|
|
||||||
|
Object.keys(unionGroup.controls).forEach(control => {
|
||||||
|
if (control === tagId) return
|
||||||
|
this.formGroup.addControl(control, unionGroup.controls[control])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
resize(): void {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.onResize.emit()
|
||||||
|
}, 250) // 250 to match transition-duration, defined in html
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'form-label',
|
selector: 'form-label',
|
||||||
templateUrl: './form-label.component.html',
|
templateUrl: './form-label.component.html',
|
||||||
styleUrls: ['./form-object.component.scss'],
|
styleUrls: ['./form-object.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class FormLabelComponent {
|
export class FormLabelComponent {
|
||||||
Range = Range
|
@Input() data!: {
|
||||||
@Input() data!: HeaderData
|
name: string
|
||||||
|
new: boolean
|
||||||
|
edited: boolean
|
||||||
|
description?: string
|
||||||
|
required?: boolean
|
||||||
|
newOptions?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
constructor(private readonly alertCtrl: AlertController) {}
|
constructor(private readonly alertCtrl: AlertController) {}
|
||||||
|
|
||||||
async presentAlertDescription(event: Event) {
|
async presentAlertDescription(event: Event) {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
const { name, description } = this.data.spec
|
const { name, description } = this.data
|
||||||
|
|
||||||
const alert = await this.alertCtrl.create({
|
const alert = await this.alertCtrl.create({
|
||||||
header: name,
|
header: name,
|
||||||
@@ -426,12 +437,6 @@ export class FormLabelComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
export function getElementId(objectId: string, key: string, index = 0): string {
|
||||||
selector: 'form-error',
|
return `${key}-${index}-${objectId}`
|
||||||
templateUrl: './form-error.component.html',
|
|
||||||
styleUrls: ['./form-object.component.scss'],
|
|
||||||
})
|
|
||||||
export class FormErrorComponent {
|
|
||||||
@Input() control!: AbstractFormGroupDirective
|
|
||||||
@Input() spec!: ValueSpec
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import { Pipe, PipeTransform } from '@angular/core'
|
||||||
|
import {
|
||||||
|
AbstractControl,
|
||||||
|
FormGroup,
|
||||||
|
UntypedFormArray,
|
||||||
|
ValidationErrors,
|
||||||
|
} from '@angular/forms'
|
||||||
|
import { IonicSafeString } from '@ionic/angular'
|
||||||
|
import { ListValueSpecOf } from 'src/app/pkg-config/config-types'
|
||||||
|
import { Range } from 'src/app/pkg-config/config-utilities'
|
||||||
|
import { getElementId } from './form-object.component'
|
||||||
|
|
||||||
|
@Pipe({
|
||||||
|
name: 'getError',
|
||||||
|
})
|
||||||
|
export class GetErrorPipe implements PipeTransform {
|
||||||
|
transform(errors: ValidationErrors, patternDesc?: string): string {
|
||||||
|
if (errors['required']) {
|
||||||
|
return 'Required'
|
||||||
|
} else if (errors['pattern']) {
|
||||||
|
return patternDesc || 'Invalid pattern'
|
||||||
|
} else if (errors['notNumber']) {
|
||||||
|
return 'Must be a number'
|
||||||
|
} else if (errors['numberNotInteger']) {
|
||||||
|
return 'Must be an integer'
|
||||||
|
} else if (errors['numberNotInRange']) {
|
||||||
|
return errors['numberNotInRange'].value
|
||||||
|
} else if (errors['listNotUnique']) {
|
||||||
|
return errors['listNotUnique'].value
|
||||||
|
} else if (errors['listNotInRange']) {
|
||||||
|
return errors['listNotInRange'].value
|
||||||
|
} else if (errors['listItemIssue']) {
|
||||||
|
return errors['listItemIssue'].value
|
||||||
|
} else {
|
||||||
|
return 'Unknown error'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Pipe({
|
||||||
|
name: 'toEnumListDisplay',
|
||||||
|
})
|
||||||
|
export class ToEnumListDisplayPipe implements PipeTransform {
|
||||||
|
transform(arr: string[], spec: ListValueSpecOf<'enum'>): string {
|
||||||
|
return arr.map((v: string) => spec['value-names'][v]).join(', ')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Pipe({
|
||||||
|
name: 'toWarningText',
|
||||||
|
})
|
||||||
|
export class ToWarningTextPipe implements PipeTransform {
|
||||||
|
transform(text?: string): IonicSafeString | string {
|
||||||
|
return text
|
||||||
|
? new IonicSafeString(`<ion-text color="warning">${text}</ion-text>`)
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Pipe({
|
||||||
|
name: 'toRange',
|
||||||
|
})
|
||||||
|
export class ToRangePipe implements PipeTransform {
|
||||||
|
transform(range: string): Range {
|
||||||
|
return Range.from(range)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Pipe({
|
||||||
|
name: 'toElementId',
|
||||||
|
})
|
||||||
|
export class ToElementIdPipe implements PipeTransform {
|
||||||
|
transform(objectId: string, key: string, index = 0): string {
|
||||||
|
return getElementId(objectId, key, index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Pipe({
|
||||||
|
name: 'getControl',
|
||||||
|
})
|
||||||
|
export class GetControlPipe implements PipeTransform {
|
||||||
|
transform(
|
||||||
|
formGroup: FormGroup,
|
||||||
|
key: string,
|
||||||
|
index?: number,
|
||||||
|
): AbstractControl {
|
||||||
|
const abstractControl = formGroup.get(key)!
|
||||||
|
if (index !== undefined)
|
||||||
|
return (abstractControl as UntypedFormArray).at(index)
|
||||||
|
return abstractControl
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<div [formGroup]="formGroup">
|
||||||
|
<!-- union enum -->
|
||||||
|
<ion-item-divider [class.error-border]="formGroup.invalid">
|
||||||
|
<form-label
|
||||||
|
[data]="{
|
||||||
|
name: spec.tag.name,
|
||||||
|
description: spec.tag.description,
|
||||||
|
new: isNew,
|
||||||
|
newOptions: hasNewOptions,
|
||||||
|
edited: formGroup.dirty
|
||||||
|
}"
|
||||||
|
></form-label>
|
||||||
|
<!-- class enter-click disables the enter click on the modal behind the select -->
|
||||||
|
<ion-select
|
||||||
|
[interfaceOptions]="{
|
||||||
|
message: spec.tag.warning | toWarningText,
|
||||||
|
cssClass: 'enter-click'
|
||||||
|
}"
|
||||||
|
slot="end"
|
||||||
|
placeholder="Select"
|
||||||
|
[formControlName]="spec.tag.id"
|
||||||
|
[selectedText]="spec.tag['variant-names'][unionValue]"
|
||||||
|
(ionChange)="updateUnion($event)"
|
||||||
|
>
|
||||||
|
<ion-select-option
|
||||||
|
*ngFor="let option of spec.variants | keyvalue"
|
||||||
|
[value]="option.key"
|
||||||
|
>
|
||||||
|
{{ spec.tag['variant-names'][option.key] }}
|
||||||
|
</ion-select-option>
|
||||||
|
</ion-select>
|
||||||
|
</ion-item-divider>
|
||||||
|
|
||||||
|
<elastic-container [id]="objectId | toElementId: 'union'" class="indent">
|
||||||
|
<form-object
|
||||||
|
[objectSpec]="spec.variants[unionValue]"
|
||||||
|
[formGroup]="formGroup"
|
||||||
|
[current]="current"
|
||||||
|
[original]="original"
|
||||||
|
(onResize)="resize()"
|
||||||
|
></form-object>
|
||||||
|
</elastic-container>
|
||||||
|
</div>
|
||||||
@@ -80,8 +80,8 @@
|
|||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
<!-- no config -->
|
<!-- no options -->
|
||||||
<ion-item *ngIf="!configForm">
|
<ion-item *ngIf="!hasOptions">
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<p>
|
<p>
|
||||||
No config options for {{ pkg.manifest.title }} {{
|
No config options for {{ pkg.manifest.title }} {{
|
||||||
@@ -111,7 +111,11 @@
|
|||||||
<ion-footer>
|
<ion-footer>
|
||||||
<ion-toolbar>
|
<ion-toolbar>
|
||||||
<ng-container *ngIf="!loading && !loadingError">
|
<ng-container *ngIf="!loading && !loadingError">
|
||||||
<ion-buttons *ngIf="configForm" slot="start" class="ion-padding-start">
|
<ion-buttons
|
||||||
|
*ngIf="configForm && hasOptions"
|
||||||
|
slot="start"
|
||||||
|
class="ion-padding-start"
|
||||||
|
>
|
||||||
<ion-button fill="clear" (click)="resetDefaults()">
|
<ion-button fill="clear" (click)="resetDefaults()">
|
||||||
<ion-icon slot="start" name="refresh"></ion-icon>
|
<ion-icon slot="start" name="refresh"></ion-icon>
|
||||||
Reset Defaults
|
Reset Defaults
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ export class AppConfigPage {
|
|||||||
saving = false
|
saving = false
|
||||||
loadingError: string | IonicSafeString = ''
|
loadingError: string | IonicSafeString = ''
|
||||||
|
|
||||||
|
hasOptions = false
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly embassyApi: ApiService,
|
private readonly embassyApi: ApiService,
|
||||||
private readonly errToast: ErrorToastService,
|
private readonly errToast: ErrorToastService,
|
||||||
@@ -101,7 +103,10 @@ export class AppConfigPage {
|
|||||||
this.configSpec,
|
this.configSpec,
|
||||||
newConfig || this.original,
|
newConfig || this.original,
|
||||||
)
|
)
|
||||||
this.configForm.markAllAsTouched()
|
|
||||||
|
this.hasOptions = !!Object.values(this.configSpec).find(
|
||||||
|
valSpec => valSpec.type !== 'pointer',
|
||||||
|
)
|
||||||
|
|
||||||
if (patch) {
|
if (patch) {
|
||||||
this.diff = this.getDiff(patch)
|
this.diff = this.getDiff(patch)
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ export class GenericFormPage {
|
|||||||
convertValuesRecursive(this.spec, this.formGroup)
|
convertValuesRecursive(this.spec, this.formGroup)
|
||||||
|
|
||||||
if (this.formGroup.invalid) {
|
if (this.formGroup.invalid) {
|
||||||
this.formGroup.markAllAsTouched()
|
|
||||||
document
|
document
|
||||||
.getElementsByClassName('validation-error')[0]
|
.getElementsByClassName('validation-error')[0]
|
||||||
?.scrollIntoView({ behavior: 'smooth' })
|
?.scrollIntoView({ behavior: 'smooth' })
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export interface ValueSpecBoolean extends WithStandalone {
|
|||||||
default: boolean
|
default: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ValueSpecUnion extends WithStandalone {
|
export interface ValueSpecUnion {
|
||||||
type: 'union'
|
type: 'union'
|
||||||
tag: UnionTagSpec
|
tag: UnionTagSpec
|
||||||
variants: { [key: string]: ConfigSpec }
|
variants: { [key: string]: ConfigSpec }
|
||||||
@@ -159,12 +159,13 @@ export interface ListValueSpecUnion {
|
|||||||
|
|
||||||
export interface UnionTagSpec {
|
export interface UnionTagSpec {
|
||||||
id: string // The name of the field containing one of the union variants
|
id: string // The name of the field containing one of the union variants
|
||||||
name: string
|
|
||||||
description?: string
|
|
||||||
'variant-names': {
|
'variant-names': {
|
||||||
// the name of each variant
|
// the name of each variant
|
||||||
[variant: string]: string
|
[variant: string]: string
|
||||||
}
|
}
|
||||||
|
name: string
|
||||||
|
description?: string
|
||||||
|
warning?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DefaultString = string | { charset: string; len: number }
|
export type DefaultString = string | { charset: string; len: number }
|
||||||
|
|||||||
@@ -291,18 +291,17 @@ export module Mock {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
bitcoinNode: {
|
bitcoinNode: {
|
||||||
name: 'Bitcoin Node Settings',
|
|
||||||
type: 'union',
|
type: 'union',
|
||||||
description: 'The node settings',
|
|
||||||
default: 'internal',
|
default: 'internal',
|
||||||
warning: 'Careful changing this',
|
|
||||||
tag: {
|
tag: {
|
||||||
id: 'type',
|
id: 'type',
|
||||||
name: 'Type',
|
|
||||||
'variant-names': {
|
'variant-names': {
|
||||||
internal: 'Internal',
|
internal: 'Internal',
|
||||||
external: 'External',
|
external: 'External',
|
||||||
},
|
},
|
||||||
|
name: 'Bitcoin Node Settings',
|
||||||
|
description: 'The node settings',
|
||||||
|
warning: 'Careful changing this',
|
||||||
},
|
},
|
||||||
variants: {
|
variants: {
|
||||||
internal: {
|
internal: {
|
||||||
@@ -1249,12 +1248,12 @@ export module Mock {
|
|||||||
spec: {
|
spec: {
|
||||||
tag: {
|
tag: {
|
||||||
id: 'preference',
|
id: 'preference',
|
||||||
name: 'Preferences',
|
|
||||||
'variant-names': {
|
'variant-names': {
|
||||||
summer: 'Summer',
|
summer: 'Summer',
|
||||||
winter: 'Winter',
|
winter: 'Winter',
|
||||||
other: 'Other',
|
other: 'Other',
|
||||||
},
|
},
|
||||||
|
name: 'Preference',
|
||||||
},
|
},
|
||||||
// this default is used to make a union selection when a new list element is first created
|
// this default is used to make a union selection when a new list element is first created
|
||||||
default: 'summer',
|
default: 'summer',
|
||||||
@@ -1425,18 +1424,17 @@ export module Mock {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
'bitcoin-node': {
|
'bitcoin-node': {
|
||||||
name: 'Bitcoin Node Settings',
|
|
||||||
type: 'union',
|
type: 'union',
|
||||||
description: 'Options<ul><li>Item 1</li><li>Item 2</li></ul>',
|
|
||||||
default: 'internal',
|
default: 'internal',
|
||||||
warning: 'Careful changing this',
|
|
||||||
tag: {
|
tag: {
|
||||||
id: 'type',
|
id: 'type',
|
||||||
name: 'Type',
|
|
||||||
'variant-names': {
|
'variant-names': {
|
||||||
internal: 'Internal',
|
internal: 'Internal',
|
||||||
external: 'External',
|
external: 'External',
|
||||||
},
|
},
|
||||||
|
name: 'Bitcoin Node Settings',
|
||||||
|
description: 'Options<ul><li>Item 1</li><li>Item 2</li></ul>',
|
||||||
|
warning: 'Careful changing this',
|
||||||
},
|
},
|
||||||
variants: {
|
variants: {
|
||||||
internal: {
|
internal: {
|
||||||
|
|||||||
@@ -345,18 +345,17 @@ export const mockPatchData: DataModel = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
bitcoinNode: {
|
bitcoinNode: {
|
||||||
name: 'Bitcoin Node Settings',
|
|
||||||
type: 'union',
|
type: 'union',
|
||||||
description: 'The node settings',
|
|
||||||
default: 'internal',
|
default: 'internal',
|
||||||
warning: 'Careful changing this',
|
|
||||||
tag: {
|
tag: {
|
||||||
id: 'type',
|
id: 'type',
|
||||||
name: 'Type',
|
|
||||||
'variant-names': {
|
'variant-names': {
|
||||||
internal: 'Internal',
|
internal: 'Internal',
|
||||||
external: 'External',
|
external: 'External',
|
||||||
},
|
},
|
||||||
|
name: 'Bitcoin Node Settings',
|
||||||
|
description: 'The node settings',
|
||||||
|
warning: 'Careful changing this',
|
||||||
},
|
},
|
||||||
variants: {
|
variants: {
|
||||||
internal: {
|
internal: {
|
||||||
|
|||||||
@@ -46,9 +46,7 @@ export class FormService {
|
|||||||
current?: { [key: string]: any } | null,
|
current?: { [key: string]: any } | null,
|
||||||
): UntypedFormGroup {
|
): UntypedFormGroup {
|
||||||
const { variants, tag } = spec
|
const { variants, tag } = spec
|
||||||
const { name, description, warning } = isFullUnion(spec)
|
const { name, description, warning, 'variant-names': variantNames } = tag
|
||||||
? spec
|
|
||||||
: { ...spec.tag, warning: undefined }
|
|
||||||
|
|
||||||
const enumSpec: ValueSpecEnum = {
|
const enumSpec: ValueSpecEnum = {
|
||||||
type: 'enum',
|
type: 'enum',
|
||||||
@@ -57,7 +55,7 @@ export class FormService {
|
|||||||
warning,
|
warning,
|
||||||
default: selection,
|
default: selection,
|
||||||
values: Object.keys(variants),
|
values: Object.keys(variants),
|
||||||
'value-names': tag['variant-names'],
|
'value-names': variantNames,
|
||||||
}
|
}
|
||||||
return this.getFormGroup(
|
return this.getFormGroup(
|
||||||
{ [spec.tag.id]: enumSpec, ...spec.variants[selection] },
|
{ [spec.tag.id]: enumSpec, ...spec.variants[selection] },
|
||||||
@@ -207,12 +205,6 @@ function listValidators(spec: ValueSpecList): ValidatorFn[] {
|
|||||||
return validators
|
return validators
|
||||||
}
|
}
|
||||||
|
|
||||||
function isFullUnion(
|
|
||||||
spec: ValueSpecUnion | ListValueSpecUnion,
|
|
||||||
): spec is ValueSpecUnion {
|
|
||||||
return !!(spec as ValueSpecUnion).name
|
|
||||||
}
|
|
||||||
|
|
||||||
export function numberInRange(stringRange: string): ValidatorFn {
|
export function numberInRange(stringRange: string): ValidatorFn {
|
||||||
return control => {
|
return control => {
|
||||||
const value = control.value
|
const value = control.value
|
||||||
@@ -461,15 +453,18 @@ function uniqueByMessageWrapper(
|
|||||||
) {
|
) {
|
||||||
let configSpec: ConfigSpec
|
let configSpec: ConfigSpec
|
||||||
if (isUnion(spec)) {
|
if (isUnion(spec)) {
|
||||||
const variantKey = obj[spec.tag.id]
|
const tagId = spec.tag.id
|
||||||
configSpec = spec.variants[variantKey]
|
configSpec = {
|
||||||
|
[tagId]: { name: spec.tag.name } as ValueSpec,
|
||||||
|
...spec.variants[obj[tagId]],
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
configSpec = spec.spec
|
configSpec = spec.spec
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = uniqueByMessage(uniqueBy, configSpec)
|
const message = uniqueByMessage(uniqueBy, configSpec)
|
||||||
if (message) {
|
if (message) {
|
||||||
return ' Must be unique by: ' + message + '.'
|
return ' Must be unique by: ' + message
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -483,7 +478,9 @@ function uniqueByMessage(
|
|||||||
if (uniqueBy === null) {
|
if (uniqueBy === null) {
|
||||||
return ''
|
return ''
|
||||||
} else if (typeof uniqueBy === 'string') {
|
} else if (typeof uniqueBy === 'string') {
|
||||||
return configSpec[uniqueBy] ? configSpec[uniqueBy].name : uniqueBy
|
return configSpec[uniqueBy]
|
||||||
|
? (configSpec[uniqueBy] as ValueSpecObject).name
|
||||||
|
: uniqueBy
|
||||||
} else if ('any' in uniqueBy) {
|
} else if ('any' in uniqueBy) {
|
||||||
joinFunc = ' OR '
|
joinFunc = ' OR '
|
||||||
for (let subSpec of uniqueBy.any) {
|
for (let subSpec of uniqueBy.any) {
|
||||||
|
|||||||
Reference in New Issue
Block a user