mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +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",
|
||||
"@ionic/angular": "^6.1.15",
|
||||
"@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/emver": "^0.1.5",
|
||||
"angular-svg-round-progressbar": "^9.0.0",
|
||||
@@ -3266,6 +3269,42 @@
|
||||
"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": {
|
||||
"version": "14.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-14.2.3.tgz",
|
||||
@@ -16891,6 +16930,30 @@
|
||||
"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": {
|
||||
"version": "14.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-14.2.3.tgz",
|
||||
|
||||
@@ -39,6 +39,9 @@
|
||||
"@angular/platform-browser-dynamic": "^14.1.0",
|
||||
"@angular/router": "^14.1.0",
|
||||
"@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",
|
||||
"@start9labs/argon2": "^0.1.0",
|
||||
"@start9labs/emver": "^0.1.5",
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
"@angular/core": ">=13.2.0",
|
||||
"@angular/router": ">=13.2.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"
|
||||
},
|
||||
"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-button.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.module'
|
||||
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
|
||||
*ngIf="data.spec.description"
|
||||
*ngIf="data.description"
|
||||
class="slot-start"
|
||||
fill="clear"
|
||||
(click)="presentAlertDescription($event)"
|
||||
@@ -7,21 +7,10 @@
|
||||
<ion-icon name="help-circle-outline" slot="icon-only" size="small"></ion-icon>
|
||||
</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.newOptions"> (New Options)</ion-text>
|
||||
<ion-text color="warning" *ngIf="data.edited"> (Edited)</ion-text>
|
||||
|
||||
<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
|
||||
>
|
||||
<span *ngIf="data.required"> * </span>
|
||||
|
||||
@@ -1,66 +1,21 @@
|
||||
<ion-item-group [formGroup]="formGroup">
|
||||
<div *ngFor="let entry of formGroup.controls | keyvalue: asIsOrder">
|
||||
<!-- union enum -->
|
||||
<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">
|
||||
<div *ngIf="objectSpec[entry.key] as spec">
|
||||
<!-- string or number -->
|
||||
<ng-container *ngIf="spec.type === 'string' || spec.type === 'number'">
|
||||
<!-- label -->
|
||||
<h4
|
||||
class="input-label"
|
||||
[class.validation-error]="formGroup.get(entry.key)?.errors"
|
||||
>
|
||||
<h4 class="input-label">
|
||||
<form-label
|
||||
[data]="{
|
||||
spec: spec,
|
||||
name: spec.name,
|
||||
description: spec.description,
|
||||
new: original?.[entry.key] === undefined,
|
||||
edited: entry.value.dirty
|
||||
edited: entry.value.dirty,
|
||||
required: !spec.nullable
|
||||
}"
|
||||
></form-label>
|
||||
</h4>
|
||||
<ion-item color="dark" class="ion-margin-bottom">
|
||||
<ion-item color="dark">
|
||||
<ion-textarea
|
||||
*ngIf="spec.type === 'string' && spec.textarea; else notTextArea"
|
||||
[placeholder]="spec.placeholder || 'Enter ' + spec.name"
|
||||
@@ -107,22 +62,21 @@
|
||||
>{{ spec.units }}</ion-note
|
||||
>
|
||||
</ion-item>
|
||||
<form-error
|
||||
*ngIf="formGroup.get(entry.key)?.errors"
|
||||
[control]="$any(formGroup.get(entry.key))"
|
||||
[spec]="spec"
|
||||
>
|
||||
</form-error>
|
||||
<p class="error-message">
|
||||
<span *ngIf="(formGroup | getControl: entry.key).errors as errors">
|
||||
{{ errors | getError: $any(spec)['pattern-description'] }}
|
||||
</span>
|
||||
</p>
|
||||
</ng-container>
|
||||
<!-- boolean or enum -->
|
||||
<ion-item
|
||||
*ngIf="['boolean', 'enum'] | includes: spec.type"
|
||||
*ngIf="spec.type === 'boolean' || spec.type === 'enum'"
|
||||
style="--padding-start: 0"
|
||||
>
|
||||
<ion-button
|
||||
*ngIf="spec.description"
|
||||
fill="clear"
|
||||
(click)="presentAlertDescription($event, spec)"
|
||||
(click)="presentAlertBoolEnumDescription($event, spec)"
|
||||
style="--padding-start: 0"
|
||||
>
|
||||
<ion-icon
|
||||
@@ -157,7 +111,7 @@
|
||||
<ion-select
|
||||
*ngIf="spec.type === 'enum' && formGroup.get(entry.key) as control"
|
||||
[interfaceOptions]="{
|
||||
message: getWarningText(spec.warning),
|
||||
message: spec.warning | toWarningText,
|
||||
cssClass: 'enter-click'
|
||||
}"
|
||||
slot="end"
|
||||
@@ -173,21 +127,21 @@
|
||||
</ion-select-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
<!-- object or union -->
|
||||
<ng-container *ngIf="spec.type === 'object' || spec.type === 'union'">
|
||||
<!-- object -->
|
||||
<ng-container *ngIf="spec.type === 'object'">
|
||||
<!-- label -->
|
||||
<ion-item-divider
|
||||
(click)="toggleExpandObject(entry.key)"
|
||||
style="cursor: pointer"
|
||||
[class.error-border]="entry.value.invalid"
|
||||
[class.validation-error]="entry.value.invalid"
|
||||
>
|
||||
<form-label
|
||||
[data]="{
|
||||
spec: spec,
|
||||
name: spec.name,
|
||||
description: spec.description,
|
||||
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>
|
||||
<ion-icon
|
||||
@@ -198,45 +152,40 @@
|
||||
transform: objectDisplay[entry.key].expanded
|
||||
? 'rotate(0deg)'
|
||||
: 'rotate(180deg)',
|
||||
transition: 'transform 0.25s ease-out'
|
||||
transition: 'transform 0.42s ease-out'
|
||||
}"
|
||||
></ion-icon>
|
||||
</ion-item-divider>
|
||||
<!-- body -->
|
||||
<div
|
||||
[id]="getElementId(entry.key)"
|
||||
[id]="objectId | toElementId: entry.key"
|
||||
[ngStyle]="{
|
||||
'max-height': objectDisplay[entry.key].height,
|
||||
overflow: 'hidden',
|
||||
'transition-property': 'max-height',
|
||||
'transition-duration': '.25s'
|
||||
'transition-duration': '.42s'
|
||||
}"
|
||||
>
|
||||
<div class="nested-wrapper">
|
||||
<form-object
|
||||
*ngIf="spec.type === 'object'"
|
||||
[objectSpec]="spec.spec"
|
||||
[formGroup]="$any(entry.value)"
|
||||
[current]="current?.[entry.key]"
|
||||
[original]="original?.[entry.key]"
|
||||
(onExpand)="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)"
|
||||
(onResize)="resize(entry.key)"
|
||||
(hasNewOptions)="setHasNew(entry.key)"
|
||||
></form-object>
|
||||
</div>
|
||||
</div>
|
||||
</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) -->
|
||||
<ng-container *ngIf="spec.type === 'list' && spec.subtype !== 'enum'">
|
||||
<ng-container
|
||||
@@ -244,15 +193,14 @@
|
||||
[formArrayName]="entry.key"
|
||||
>
|
||||
<!-- label -->
|
||||
<ion-item-divider
|
||||
[class.error-border]="entry.value.invalid"
|
||||
[class.validation-error]="entry.value.invalid"
|
||||
>
|
||||
<ion-item-divider [class.error-border]="entry.value.invalid">
|
||||
<form-label
|
||||
[data]="{
|
||||
spec: spec,
|
||||
name: spec.name,
|
||||
description: spec.description,
|
||||
new: original?.[entry.key] === undefined,
|
||||
edited: entry.value.dirty
|
||||
edited: entry.value.dirty,
|
||||
required: !!(spec.range | toRange).min
|
||||
}"
|
||||
></form-label>
|
||||
<ion-button
|
||||
@@ -266,12 +214,11 @@
|
||||
Add
|
||||
</ion-button>
|
||||
</ion-item-divider>
|
||||
<form-error
|
||||
*ngIf="formGroup.get(entry.key)?.errors"
|
||||
[control]="$any(formGroup.get(entry.key))"
|
||||
[spec]="spec"
|
||||
>
|
||||
</form-error>
|
||||
<p class="error-message" style="margin-bottom: 8px">
|
||||
<span *ngIf="(formGroup | getControl: entry.key).errors as errors">
|
||||
{{ errors | getError }}
|
||||
</span>
|
||||
</p>
|
||||
<!-- body -->
|
||||
<div class="nested-wrapper">
|
||||
<div
|
||||
@@ -279,13 +226,12 @@
|
||||
let abstractControl of $any(formArr).controls;
|
||||
let i = index
|
||||
"
|
||||
class="ion-padding-top"
|
||||
>
|
||||
<!-- nested -->
|
||||
<!-- object or union -->
|
||||
<ng-container
|
||||
*ngIf="spec.subtype === 'object' || spec.subtype === 'union'"
|
||||
>
|
||||
<!-- nested label -->
|
||||
<!-- object/union label -->
|
||||
<ion-item
|
||||
button
|
||||
(click)="toggleExpandListObject(entry.key, i)"
|
||||
@@ -293,11 +239,9 @@
|
||||
>
|
||||
<form-label
|
||||
[data]="{
|
||||
spec: $any({
|
||||
name:
|
||||
objectListDisplay[entry.key][i].displayAs ||
|
||||
'Entry ' + (i + 1)
|
||||
}),
|
||||
name:
|
||||
objectListDisplay[entry.key][i].displayAs ||
|
||||
'Entry ' + (i + 1),
|
||||
new: false,
|
||||
edited: abstractControl.dirty
|
||||
}"
|
||||
@@ -310,41 +254,43 @@
|
||||
transform: objectListDisplay[entry.key][i].expanded
|
||||
? 'rotate(0deg)'
|
||||
: 'rotate(180deg)',
|
||||
transition: 'transform 0.25s ease-out'
|
||||
transition: 'transform 0.42s ease-out'
|
||||
}"
|
||||
></ion-icon>
|
||||
</ion-item>
|
||||
<!-- nested body -->
|
||||
<!-- object/union body -->
|
||||
<div
|
||||
style="padding-left: 24px"
|
||||
[id]="getElementId(entry.key, i)"
|
||||
[id]="objectId | toElementId: entry.key:i"
|
||||
[ngStyle]="{
|
||||
'max-height': objectListDisplay[entry.key][i].height,
|
||||
overflow: 'hidden',
|
||||
'transition-property': 'max-height',
|
||||
'transition-duration': '.25s'
|
||||
'transition-duration': '.42s'
|
||||
}"
|
||||
>
|
||||
<form-object
|
||||
[objectSpec]="
|
||||
spec.subtype === 'union'
|
||||
? $any(spec.spec).variants[
|
||||
abstractControl.controls[$any(spec.spec).tag.id]
|
||||
.value
|
||||
]
|
||||
: $any(spec.spec).spec
|
||||
"
|
||||
*ngIf="spec.subtype === 'object'"
|
||||
[objectSpec]="$any(spec.spec).spec"
|
||||
[formGroup]="abstractControl"
|
||||
[current]="current?.[entry.key]?.[i]"
|
||||
[original]="original?.[entry.key]?.[i]"
|
||||
[unionSpec]="
|
||||
spec.subtype === 'union' ? $any(spec.spec) : undefined
|
||||
"
|
||||
(onInputChange)="
|
||||
updateLabel(entry.key, i, $any(spec.spec)['display-as'])
|
||||
"
|
||||
(onExpand)="resize(entry.key, i)"
|
||||
(onResize)="resize(entry.key, i)"
|
||||
></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">
|
||||
<ion-button
|
||||
fill="clear"
|
||||
@@ -358,9 +304,9 @@
|
||||
</div>
|
||||
</ng-container>
|
||||
<!-- string or number -->
|
||||
<ion-item-group
|
||||
[id]="getElementId(entry.key, i)"
|
||||
<div
|
||||
*ngIf="spec.subtype === 'string' || spec.subtype === 'number'"
|
||||
[id]="objectId | toElementId: entry.key:i"
|
||||
>
|
||||
<ion-item color="dark">
|
||||
<ion-input
|
||||
@@ -382,13 +328,16 @@
|
||||
<ion-icon slot="icon-only" name="close"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
<form-error
|
||||
*ngIf="abstractControl.errors"
|
||||
[control]="abstractControl"
|
||||
[spec]="$any(spec.spec)"
|
||||
>
|
||||
</form-error>
|
||||
</ion-item-group>
|
||||
<p class="error-message">
|
||||
<span
|
||||
*ngIf="
|
||||
(formGroup | getControl: entry.key:i).errors as errors
|
||||
"
|
||||
>
|
||||
{{ errors | getError: $any(spec)['pattern-description'] }}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
@@ -402,9 +351,9 @@
|
||||
<!-- label -->
|
||||
<p class="input-label">
|
||||
<form-label
|
||||
[class.validation-error]="entry.value.invalid"
|
||||
[data]="{
|
||||
spec: spec,
|
||||
name: spec.name,
|
||||
description: spec.description,
|
||||
new: original?.[entry.key] === undefined,
|
||||
edited: entry.value.dirty
|
||||
}"
|
||||
@@ -418,18 +367,17 @@
|
||||
(click)="presentModalEnumList(entry.key, $any(spec), formArr.value)"
|
||||
>
|
||||
<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-button slot="end" fill="clear" color="light">
|
||||
<ion-icon slot="icon-only" name="chevron-down"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
<form-error
|
||||
*ngIf="formGroup.get(entry.key)?.errors"
|
||||
[control]="$any(formGroup.get(entry.key))"
|
||||
[spec]="spec"
|
||||
>
|
||||
</form-error>
|
||||
<p class="error-message">
|
||||
<span *ngIf="(formGroup | getControl: entry.key).errors as errors">
|
||||
{{ errors | getError }}
|
||||
</span>
|
||||
</p>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
@@ -2,16 +2,34 @@ import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import {
|
||||
FormObjectComponent,
|
||||
FormUnionComponent,
|
||||
FormLabelComponent,
|
||||
FormErrorComponent,
|
||||
} from './form-object.component'
|
||||
import {
|
||||
GetErrorPipe,
|
||||
ToWarningTextPipe,
|
||||
ToElementIdPipe,
|
||||
GetControlPipe,
|
||||
ToEnumListDisplayPipe,
|
||||
ToRangePipe,
|
||||
} from './form-object.pipes'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
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'
|
||||
|
||||
@NgModule({
|
||||
declarations: [FormObjectComponent, FormLabelComponent, FormErrorComponent],
|
||||
declarations: [
|
||||
FormObjectComponent,
|
||||
FormUnionComponent,
|
||||
FormLabelComponent,
|
||||
ToWarningTextPipe,
|
||||
GetErrorPipe,
|
||||
ToEnumListDisplayPipe,
|
||||
ToElementIdPipe,
|
||||
GetControlPipe,
|
||||
ToRangePipe,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
@@ -19,7 +37,8 @@ import { EnumListPageModule } from 'src/app/modals/enum-list/enum-list.module'
|
||||
ReactiveFormsModule,
|
||||
SharedPipesModule,
|
||||
EnumListPageModule,
|
||||
ElasticContainerModule,
|
||||
],
|
||||
exports: [FormObjectComponent, FormLabelComponent, FormErrorComponent],
|
||||
exports: [FormObjectComponent, FormLabelComponent],
|
||||
})
|
||||
export class FormObjectComponentModule {}
|
||||
|
||||
@@ -31,16 +31,10 @@ ion-item-divider {
|
||||
padding: 0 0 16px 24px;
|
||||
}
|
||||
|
||||
.validation-error {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
p {
|
||||
margin-bottom: 4px;
|
||||
font-size: small;
|
||||
color: var(--ion-color-danger);
|
||||
}
|
||||
margin-top: 2px;
|
||||
font-size: small;
|
||||
color: var(--ion-color-danger);
|
||||
}
|
||||
|
||||
.indent {
|
||||
|
||||
@@ -1,30 +1,28 @@
|
||||
import { Component, Input, Output, EventEmitter } from '@angular/core'
|
||||
import {
|
||||
AbstractFormGroupDirective,
|
||||
FormArray,
|
||||
UntypedFormArray,
|
||||
UntypedFormGroup,
|
||||
} from '@angular/forms'
|
||||
import {
|
||||
AlertButton,
|
||||
AlertController,
|
||||
IonicSafeString,
|
||||
ModalController,
|
||||
} from '@ionic/angular'
|
||||
Component,
|
||||
Input,
|
||||
Output,
|
||||
EventEmitter,
|
||||
ChangeDetectionStrategy,
|
||||
Inject,
|
||||
} from '@angular/core'
|
||||
import { FormArray, UntypedFormArray, UntypedFormGroup } from '@angular/forms'
|
||||
import { AlertButton, AlertController, ModalController } from '@ionic/angular'
|
||||
import {
|
||||
ConfigSpec,
|
||||
ListValueSpecOf,
|
||||
ValueSpec,
|
||||
ValueSpecBoolean,
|
||||
ValueSpecEnum,
|
||||
ValueSpecList,
|
||||
ValueSpecListOf,
|
||||
ValueSpecUnion,
|
||||
} from 'src/app/pkg-config/config-types'
|
||||
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 { pauseFor } from '@start9labs/shared'
|
||||
import { v4 } from 'uuid'
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
const Mustache = require('mustache')
|
||||
|
||||
interface Config {
|
||||
@@ -38,11 +36,10 @@ interface Config {
|
||||
export class FormObjectComponent {
|
||||
@Input() objectSpec!: ConfigSpec
|
||||
@Input() formGroup!: UntypedFormGroup
|
||||
@Input() unionSpec?: ValueSpecUnion
|
||||
@Input() current?: Config
|
||||
@Input() original?: Config
|
||||
@Output() onInputChange = new EventEmitter<void>()
|
||||
@Output() onExpand = new EventEmitter<void>()
|
||||
@Output() onResize = new EventEmitter<void>()
|
||||
@Output() hasNewOptions = new EventEmitter<void>()
|
||||
warningAck: { [key: string]: boolean } = {}
|
||||
unmasked: { [key: string]: boolean } = {}
|
||||
@@ -52,14 +49,13 @@ export class FormObjectComponent {
|
||||
objectListDisplay: {
|
||||
[key: string]: { expanded: boolean; height: string; displayAs: string }[]
|
||||
} = {}
|
||||
private objectId = v4()
|
||||
|
||||
Object = Object
|
||||
objectId = v4()
|
||||
|
||||
constructor(
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly formService: FormService,
|
||||
@Inject(DOCUMENT) private readonly document: Document,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
@@ -80,7 +76,7 @@ export class FormObjectComponent {
|
||||
: '',
|
||||
}
|
||||
})
|
||||
} else if (['object', 'union'].includes(spec.type)) {
|
||||
} else if (spec.type === 'object') {
|
||||
this.objectDisplay[key] = {
|
||||
expanded: false,
|
||||
height: '0px',
|
||||
@@ -101,64 +97,30 @@ export class FormObjectComponent {
|
||||
}, 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 {
|
||||
setTimeout(() => {
|
||||
if (i !== undefined) {
|
||||
this.objectListDisplay[key][i].height = this.getDocSize(key, i)
|
||||
this.objectListDisplay[key][i].height = this.getScrollHeight(key, i)
|
||||
} else {
|
||||
this.objectDisplay[key].height = this.getDocSize(key)
|
||||
this.objectDisplay[key].height = this.getScrollHeight(key)
|
||||
}
|
||||
this.onExpand.emit()
|
||||
}, 250) // 250 to match transition-duration, defined in html
|
||||
this.onResize.emit()
|
||||
}, 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))
|
||||
}
|
||||
|
||||
toggleExpandObject(key: string) {
|
||||
this.objectDisplay[key].expanded = !this.objectDisplay[key].expanded
|
||||
this.objectDisplay[key].height = this.objectDisplay[key].expanded
|
||||
? this.getDocSize(key)
|
||||
? this.getScrollHeight(key)
|
||||
: '0px'
|
||||
this.onExpand.emit()
|
||||
this.onResize.emit()
|
||||
}
|
||||
|
||||
toggleExpandListObject(key: string, i: number) {
|
||||
@@ -166,9 +128,9 @@ export class FormObjectComponent {
|
||||
!this.objectListDisplay[key][i].expanded
|
||||
this.objectListDisplay[key][i].height = this.objectListDisplay[key][i]
|
||||
.expanded
|
||||
? this.getDocSize(key, i)
|
||||
? this.getScrollHeight(key, i)
|
||||
: '0px'
|
||||
this.onExpand.emit()
|
||||
this.onResize.emit()
|
||||
}
|
||||
|
||||
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() {
|
||||
this.onInputChange.emit()
|
||||
}
|
||||
@@ -224,9 +180,9 @@ export class FormObjectComponent {
|
||||
await modal.present()
|
||||
}
|
||||
|
||||
async presentAlertChangeWarning(
|
||||
async presentAlertChangeWarning<T extends ValueSpec>(
|
||||
key: string,
|
||||
spec: ValueSpec,
|
||||
spec: T extends ValueSpecUnion ? never : T,
|
||||
okFn?: Function,
|
||||
cancelFn?: Function,
|
||||
) {
|
||||
@@ -282,7 +238,10 @@ export class FormObjectComponent {
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
async presentAlertDescription(event: Event, spec: ValueSpec) {
|
||||
async presentAlertBoolEnumDescription(
|
||||
event: Event,
|
||||
spec: ValueSpecBoolean | ValueSpecEnum,
|
||||
) {
|
||||
event.stopPropagation()
|
||||
const { name, description } = spec
|
||||
|
||||
@@ -318,15 +277,18 @@ export class FormObjectComponent {
|
||||
})
|
||||
}
|
||||
|
||||
this.onExpand.emit()
|
||||
|
||||
pauseFor(400).then(() => {
|
||||
const element = document.getElementById(this.getElementId(key, index))
|
||||
setTimeout(() => {
|
||||
const element = this.document.getElementById(
|
||||
getElementId(this.objectId, key, index),
|
||||
)
|
||||
element?.parentElement?.scrollIntoView({ behavior: 'smooth' })
|
||||
})
|
||||
|
||||
if (['object', 'union'].includes(listSpec.subtype)) {
|
||||
pauseFor(250).then(() => this.toggleExpandListObject(key, index))
|
||||
}
|
||||
}, 100)
|
||||
|
||||
arr.markAsDirty()
|
||||
newItem.markAllAsTouched()
|
||||
}
|
||||
|
||||
private deleteListItem(key: string, index: number, markDirty = true): void {
|
||||
@@ -360,57 +322,106 @@ export class FormObjectComponent {
|
||||
})
|
||||
|
||||
arr.markAsDirty()
|
||||
arr.markAllAsTouched()
|
||||
}
|
||||
|
||||
private getDocSize(key: string, index = 0): string {
|
||||
const element = document.getElementById(this.getElementId(key, index))
|
||||
private getScrollHeight(key: string, index = 0): string {
|
||||
const element = this.document.getElementById(
|
||||
getElementId(this.objectId, key, index),
|
||||
)
|
||||
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() {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
interface HeaderData {
|
||||
spec: ValueSpec
|
||||
edited: boolean
|
||||
new: boolean
|
||||
newOptions?: boolean
|
||||
@Component({
|
||||
selector: 'form-union',
|
||||
templateUrl: './form-union.component.html',
|
||||
styleUrls: ['./form-object.component.scss'],
|
||||
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({
|
||||
selector: 'form-label',
|
||||
templateUrl: './form-label.component.html',
|
||||
styleUrls: ['./form-object.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class FormLabelComponent {
|
||||
Range = Range
|
||||
@Input() data!: HeaderData
|
||||
@Input() data!: {
|
||||
name: string
|
||||
new: boolean
|
||||
edited: boolean
|
||||
description?: string
|
||||
required?: boolean
|
||||
newOptions?: boolean
|
||||
}
|
||||
|
||||
constructor(private readonly alertCtrl: AlertController) {}
|
||||
|
||||
async presentAlertDescription(event: Event) {
|
||||
event.stopPropagation()
|
||||
const { name, description } = this.data.spec
|
||||
const { name, description } = this.data
|
||||
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: name,
|
||||
@@ -426,12 +437,6 @@ export class FormLabelComponent {
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'form-error',
|
||||
templateUrl: './form-error.component.html',
|
||||
styleUrls: ['./form-object.component.scss'],
|
||||
})
|
||||
export class FormErrorComponent {
|
||||
@Input() control!: AbstractFormGroupDirective
|
||||
@Input() spec!: ValueSpec
|
||||
export function getElementId(objectId: string, key: string, index = 0): string {
|
||||
return `${key}-${index}-${objectId}`
|
||||
}
|
||||
|
||||
@@ -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-item>
|
||||
|
||||
<!-- no config -->
|
||||
<ion-item *ngIf="!configForm">
|
||||
<!-- no options -->
|
||||
<ion-item *ngIf="!hasOptions">
|
||||
<ion-label>
|
||||
<p>
|
||||
No config options for {{ pkg.manifest.title }} {{
|
||||
@@ -111,7 +111,11 @@
|
||||
<ion-footer>
|
||||
<ion-toolbar>
|
||||
<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-icon slot="start" name="refresh"></ion-icon>
|
||||
Reset Defaults
|
||||
|
||||
@@ -53,6 +53,8 @@ export class AppConfigPage {
|
||||
saving = false
|
||||
loadingError: string | IonicSafeString = ''
|
||||
|
||||
hasOptions = false
|
||||
|
||||
constructor(
|
||||
private readonly embassyApi: ApiService,
|
||||
private readonly errToast: ErrorToastService,
|
||||
@@ -101,7 +103,10 @@ export class AppConfigPage {
|
||||
this.configSpec,
|
||||
newConfig || this.original,
|
||||
)
|
||||
this.configForm.markAllAsTouched()
|
||||
|
||||
this.hasOptions = !!Object.values(this.configSpec).find(
|
||||
valSpec => valSpec.type !== 'pointer',
|
||||
)
|
||||
|
||||
if (patch) {
|
||||
this.diff = this.getDiff(patch)
|
||||
|
||||
@@ -48,7 +48,6 @@ export class GenericFormPage {
|
||||
convertValuesRecursive(this.spec, this.formGroup)
|
||||
|
||||
if (this.formGroup.invalid) {
|
||||
this.formGroup.markAllAsTouched()
|
||||
document
|
||||
.getElementsByClassName('validation-error')[0]
|
||||
?.scrollIntoView({ behavior: 'smooth' })
|
||||
|
||||
@@ -53,7 +53,7 @@ export interface ValueSpecBoolean extends WithStandalone {
|
||||
default: boolean
|
||||
}
|
||||
|
||||
export interface ValueSpecUnion extends WithStandalone {
|
||||
export interface ValueSpecUnion {
|
||||
type: 'union'
|
||||
tag: UnionTagSpec
|
||||
variants: { [key: string]: ConfigSpec }
|
||||
@@ -159,12 +159,13 @@ export interface ListValueSpecUnion {
|
||||
|
||||
export interface UnionTagSpec {
|
||||
id: string // The name of the field containing one of the union variants
|
||||
name: string
|
||||
description?: string
|
||||
'variant-names': {
|
||||
// the name of each variant
|
||||
[variant: string]: string
|
||||
}
|
||||
name: string
|
||||
description?: string
|
||||
warning?: string
|
||||
}
|
||||
|
||||
export type DefaultString = string | { charset: string; len: number }
|
||||
|
||||
@@ -291,18 +291,17 @@ export module Mock {
|
||||
},
|
||||
},
|
||||
bitcoinNode: {
|
||||
name: 'Bitcoin Node Settings',
|
||||
type: 'union',
|
||||
description: 'The node settings',
|
||||
default: 'internal',
|
||||
warning: 'Careful changing this',
|
||||
tag: {
|
||||
id: 'type',
|
||||
name: 'Type',
|
||||
'variant-names': {
|
||||
internal: 'Internal',
|
||||
external: 'External',
|
||||
},
|
||||
name: 'Bitcoin Node Settings',
|
||||
description: 'The node settings',
|
||||
warning: 'Careful changing this',
|
||||
},
|
||||
variants: {
|
||||
internal: {
|
||||
@@ -1249,12 +1248,12 @@ export module Mock {
|
||||
spec: {
|
||||
tag: {
|
||||
id: 'preference',
|
||||
name: 'Preferences',
|
||||
'variant-names': {
|
||||
summer: 'Summer',
|
||||
winter: 'Winter',
|
||||
other: 'Other',
|
||||
},
|
||||
name: 'Preference',
|
||||
},
|
||||
// this default is used to make a union selection when a new list element is first created
|
||||
default: 'summer',
|
||||
@@ -1425,18 +1424,17 @@ export module Mock {
|
||||
},
|
||||
},
|
||||
'bitcoin-node': {
|
||||
name: 'Bitcoin Node Settings',
|
||||
type: 'union',
|
||||
description: 'Options<ul><li>Item 1</li><li>Item 2</li></ul>',
|
||||
default: 'internal',
|
||||
warning: 'Careful changing this',
|
||||
tag: {
|
||||
id: 'type',
|
||||
name: 'Type',
|
||||
'variant-names': {
|
||||
internal: 'Internal',
|
||||
external: 'External',
|
||||
},
|
||||
name: 'Bitcoin Node Settings',
|
||||
description: 'Options<ul><li>Item 1</li><li>Item 2</li></ul>',
|
||||
warning: 'Careful changing this',
|
||||
},
|
||||
variants: {
|
||||
internal: {
|
||||
|
||||
@@ -345,18 +345,17 @@ export const mockPatchData: DataModel = {
|
||||
},
|
||||
},
|
||||
bitcoinNode: {
|
||||
name: 'Bitcoin Node Settings',
|
||||
type: 'union',
|
||||
description: 'The node settings',
|
||||
default: 'internal',
|
||||
warning: 'Careful changing this',
|
||||
tag: {
|
||||
id: 'type',
|
||||
name: 'Type',
|
||||
'variant-names': {
|
||||
internal: 'Internal',
|
||||
external: 'External',
|
||||
},
|
||||
name: 'Bitcoin Node Settings',
|
||||
description: 'The node settings',
|
||||
warning: 'Careful changing this',
|
||||
},
|
||||
variants: {
|
||||
internal: {
|
||||
|
||||
@@ -46,9 +46,7 @@ export class FormService {
|
||||
current?: { [key: string]: any } | null,
|
||||
): UntypedFormGroup {
|
||||
const { variants, tag } = spec
|
||||
const { name, description, warning } = isFullUnion(spec)
|
||||
? spec
|
||||
: { ...spec.tag, warning: undefined }
|
||||
const { name, description, warning, 'variant-names': variantNames } = tag
|
||||
|
||||
const enumSpec: ValueSpecEnum = {
|
||||
type: 'enum',
|
||||
@@ -57,7 +55,7 @@ export class FormService {
|
||||
warning,
|
||||
default: selection,
|
||||
values: Object.keys(variants),
|
||||
'value-names': tag['variant-names'],
|
||||
'value-names': variantNames,
|
||||
}
|
||||
return this.getFormGroup(
|
||||
{ [spec.tag.id]: enumSpec, ...spec.variants[selection] },
|
||||
@@ -207,12 +205,6 @@ function listValidators(spec: ValueSpecList): ValidatorFn[] {
|
||||
return validators
|
||||
}
|
||||
|
||||
function isFullUnion(
|
||||
spec: ValueSpecUnion | ListValueSpecUnion,
|
||||
): spec is ValueSpecUnion {
|
||||
return !!(spec as ValueSpecUnion).name
|
||||
}
|
||||
|
||||
export function numberInRange(stringRange: string): ValidatorFn {
|
||||
return control => {
|
||||
const value = control.value
|
||||
@@ -461,15 +453,18 @@ function uniqueByMessageWrapper(
|
||||
) {
|
||||
let configSpec: ConfigSpec
|
||||
if (isUnion(spec)) {
|
||||
const variantKey = obj[spec.tag.id]
|
||||
configSpec = spec.variants[variantKey]
|
||||
const tagId = spec.tag.id
|
||||
configSpec = {
|
||||
[tagId]: { name: spec.tag.name } as ValueSpec,
|
||||
...spec.variants[obj[tagId]],
|
||||
}
|
||||
} else {
|
||||
configSpec = spec.spec
|
||||
}
|
||||
|
||||
const message = uniqueByMessage(uniqueBy, configSpec)
|
||||
if (message) {
|
||||
return ' Must be unique by: ' + message + '.'
|
||||
return ' Must be unique by: ' + message
|
||||
}
|
||||
}
|
||||
|
||||
@@ -483,7 +478,9 @@ function uniqueByMessage(
|
||||
if (uniqueBy === null) {
|
||||
return ''
|
||||
} 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) {
|
||||
joinFunc = ' OR '
|
||||
for (let subSpec of uniqueBy.any) {
|
||||
|
||||
Reference in New Issue
Block a user