update display obj on union change (#2224)

* update display obj on union change

* deelete unnecessary changes

* more efficient

* fix: properly change height of form object

* more config examples

---------

Co-authored-by: waterplea <alexander@inkin.ru>
This commit is contained in:
Matt Hill
2023-03-17 09:57:26 -06:00
committed by GitHub
parent d0ba0936ca
commit 4d87ee2bb6
6 changed files with 196 additions and 98 deletions

View File

@@ -22,8 +22,7 @@
[formControlName]="entry.key"
(ionFocus)="presentAlertChangeWarning(entry.key, spec)"
(ionChange)="handleInputChange()"
>
</ion-textarea>
></ion-textarea>
<ng-template #notTextArea>
<ion-input
type="text"
@@ -38,8 +37,7 @@
[formControlName]="entry.key"
(ionFocus)="presentAlertChangeWarning(entry.key, spec)"
(ionChange)="handleInputChange()"
>
</ion-input>
></ion-input>
</ng-template>
<ion-button
*ngIf="spec.type === 'string' && spec.masked"
@@ -59,8 +57,9 @@
slot="end"
color="light"
style="font-size: medium"
>{{ spec.units }}</ion-note
>
{{ spec.units }}
</ion-note>
</ion-item>
<p class="error-message">
<span *ngIf="(formGroup | getControl: entry.key).errors as errors">
@@ -92,11 +91,11 @@
*ngIf="original?.[entry.key] === undefined"
color="success"
>
(New)</ion-text
>
(New)
</ion-text>
<ion-text *ngIf="entry.value.dirty" color="warning">
(Edited)</ion-text
>
(Edited)
</ion-text>
</b>
</ion-label>
<!-- boolean -->
@@ -157,14 +156,9 @@
></ion-icon>
</ion-item-divider>
<!-- body -->
<div
<tui-expand
[expanded]="objectDisplay[entry.key].expanded"
[id]="objectId | toElementId: entry.key"
[ngStyle]="{
'max-height': objectDisplay[entry.key].height,
overflow: 'hidden',
'transition-property': 'max-height',
'transition-duration': '.42s'
}"
>
<div class="nested-wrapper">
<form-object
@@ -172,11 +166,10 @@
[formGroup]="$any(entry.value)"
[current]="current?.[entry.key]"
[original]="original?.[entry.key]"
(onResize)="resize(entry.key)"
(hasNewOptions)="setHasNew(entry.key)"
></form-object>
</div>
</div>
</tui-expand>
</ng-container>
<!-- union -->
<form-union
@@ -259,15 +252,10 @@
></ion-icon>
</ion-item>
<!-- object/union body -->
<div
<tui-expand
style="padding-left: 24px"
[expanded]="objectListDisplay[entry.key][i].expanded"
[id]="objectId | toElementId: entry.key:i"
[ngStyle]="{
'max-height': objectListDisplay[entry.key][i].height,
overflow: 'hidden',
'transition-property': 'max-height',
'transition-duration': '.42s'
}"
>
<form-object
*ngIf="spec.subtype === 'object'"
@@ -278,7 +266,6 @@
(onInputChange)="
updateLabel(entry.key, i, $any(spec.spec)['display-as'])
"
(onResize)="resize(entry.key, i)"
></form-object>
<form-union
*ngIf="spec.subtype === 'union'"
@@ -289,7 +276,6 @@
(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
@@ -301,7 +287,7 @@
Delete
</ion-button>
</div>
</div>
</tui-expand>
</ng-container>
<!-- string or number -->
<div
@@ -318,8 +304,7 @@
$any(spec.spec).placeholder || 'Enter ' + spec.name
"
[formControlName]="i"
>
</ion-input>
></ion-input>
<ion-button
strong
fill="clear"

View File

@@ -18,6 +18,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { SharedPipesModule } from '@start9labs/shared'
import { TuiElasticContainerModule } from '@taiga-ui/kit'
import { EnumListPageModule } from 'src/app/modals/enum-list/enum-list.module'
import { TuiExpandModule } from '@taiga-ui/core'
@NgModule({
declarations: [
@@ -39,6 +40,7 @@ import { EnumListPageModule } from 'src/app/modals/enum-list/enum-list.module'
SharedPipesModule,
EnumListPageModule,
TuiElasticContainerModule,
TuiExpandModule,
],
exports: [FormObjectComponent, FormLabelComponent],
})

View File

@@ -6,6 +6,7 @@ import {
ChangeDetectionStrategy,
Inject,
inject,
SimpleChanges,
} from '@angular/core'
import { FormArray, UntypedFormArray, UntypedFormGroup } from '@angular/forms'
import { AlertButton, AlertController, ModalController } from '@ionic/angular'
@@ -41,15 +42,14 @@ export class FormObjectComponent {
@Input() current?: Config
@Input() original?: Config
@Output() onInputChange = new EventEmitter<void>()
@Output() onResize = new EventEmitter<void>()
@Output() hasNewOptions = new EventEmitter<void>()
warningAck: { [key: string]: boolean } = {}
unmasked: { [key: string]: boolean } = {}
objectDisplay: {
[key: string]: { expanded: boolean; height: string; hasNewOptions: boolean }
[key: string]: { expanded: boolean; hasNewOptions: boolean }
} = {}
objectListDisplay: {
[key: string]: { expanded: boolean; height: string; displayAs: string }[]
[key: string]: { expanded: boolean; displayAs: string }[]
} = {}
objectId = v4()
@@ -63,6 +63,37 @@ export class FormObjectComponent {
) {}
ngOnInit() {
this.setDisplays()
// setTimeout hack to avoid ExpressionChangedAfterItHasBeenCheckedError
setTimeout(() => {
if (
this.original &&
Object.keys(this.current || {}).some(
key => this.original![key] === undefined,
)
)
this.hasNewOptions.emit()
})
}
ngOnChanges(changes: SimpleChanges) {
const specChanges = changes['objectSpec']
if (!specChanges) return
if (
!specChanges.firstChange &&
Object.keys({
...specChanges.previousValue,
...specChanges.currentValue,
}).length !== Object.keys(specChanges.previousValue).length
) {
this.setDisplays()
}
}
private setDisplays() {
Object.keys(this.objectSpec).forEach(key => {
const spec = this.objectSpec[key]
@@ -74,7 +105,6 @@ export class FormObjectComponent {
]
this.objectListDisplay[key][index] = {
expanded: false,
height: '0px',
displayAs: displayAs
? (Mustache as any).render(displayAs, obj)
: '',
@@ -83,33 +113,10 @@ export class FormObjectComponent {
} else if (spec.type === 'object') {
this.objectDisplay[key] = {
expanded: false,
height: '0px',
hasNewOptions: false,
}
}
})
// setTimeout hack to avoid ExpressionChangedAfterItHasBeenCheckedError
setTimeout(() => {
if (this.original) {
Object.keys(this.current || {}).forEach(key => {
if ((this.original as Config)[key] === undefined) {
this.hasNewOptions.emit()
}
})
}
}, 10)
}
resize(key: string, i?: number): void {
setTimeout(() => {
if (i !== undefined) {
this.objectListDisplay[key][i].height = this.getScrollHeight(key, i)
} else {
this.objectDisplay[key].height = this.getScrollHeight(key)
}
this.onResize.emit()
}, 250) // 250 to match transition-duration defined in html, for smooth recursive resize
}
addListItemWrapper<T extends ValueSpec>(
@@ -121,20 +128,11 @@ export class FormObjectComponent {
toggleExpandObject(key: string) {
this.objectDisplay[key].expanded = !this.objectDisplay[key].expanded
this.objectDisplay[key].height = this.objectDisplay[key].expanded
? this.getScrollHeight(key)
: '0px'
this.onResize.emit()
}
toggleExpandListObject(key: string, i: number) {
this.objectListDisplay[key][i].expanded =
!this.objectListDisplay[key][i].expanded
this.objectListDisplay[key][i].height = this.objectListDisplay[key][i]
.expanded
? this.getScrollHeight(key, i)
: '0px'
this.onResize.emit()
}
updateLabel(key: string, i: number, displayAs: string) {
@@ -275,7 +273,6 @@ export class FormObjectComponent {
'display-as'
]
this.objectListDisplay[key].push({
height: '0px',
expanded: false,
displayAs: displayAs ? Mustache.render(displayAs, newItem.value) : '',
})
@@ -296,8 +293,8 @@ export class FormObjectComponent {
}
private deleteListItem(key: string, index: number, markDirty = true): void {
if (this.objectListDisplay[key])
this.objectListDisplay[key][index].height = '0px'
// if (this.objectListDisplay[key])
// this.objectListDisplay[key][index].height = '0px'
const arr = this.formGroup.get(key) as UntypedFormArray
if (markDirty) arr.markAsDirty()
pauseFor(250).then(() => {
@@ -328,13 +325,6 @@ export class FormObjectComponent {
arr.markAsDirty()
}
private getScrollHeight(key: string, index = 0): string {
const element = this.document.getElementById(
getElementId(this.objectId, key, index),
)
return `${element?.scrollHeight}px`
}
asIsOrder() {
return 0
}
@@ -352,8 +342,6 @@ export class FormUnionComponent {
@Input() current?: Config
@Input() original?: Config
@Output() onResize = new EventEmitter<void>()
get unionValue() {
return this.formGroup.get(this.spec.tag.id)?.value
}
@@ -374,10 +362,7 @@ export class FormUnionComponent {
objectId = v4()
constructor(
private readonly formService: FormService,
@Inject(DOCUMENT) private readonly document: Document,
) {}
constructor(private readonly formService: FormService) {}
updateUnion(e: any): void {
const tagId = this.spec.tag.id
@@ -397,12 +382,6 @@ export class FormUnionComponent {
this.formGroup.addControl(control, unionGroup.controls[control])
})
}
resize(): void {
setTimeout(() => {
this.onResize.emit()
}, 250) // 250 to match transition-duration, defined in html
}
}
@Component({

View File

@@ -37,7 +37,6 @@
[formGroup]="formGroup"
[current]="current"
[original]="original"
(onResize)="resize()"
></form-object>
</tui-elastic-container>
</div>

View File

@@ -1169,6 +1169,113 @@ export module Mock {
} as any // @TODO why is this necessary?
export const ConfigSpec: RR.GetPackageConfigRes['spec'] = {
bitcoin: {
type: 'object',
name: 'Bitcoin Settings',
description:
'RPC and P2P interface configuration options for Bitcoin Core',
spec: {
'bitcoind-p2p': {
type: 'union',
tag: {
id: 'type',
name: 'Bitcoin Core P2P',
description:
'<p>The Bitcoin Core node to connect to over the peer-to-peer (P2P) interface:</p><ul><li><strong>Bitcoin Core</strong>: The Bitcoin Core service installed on this device</li><li><strong>External Node</strong>: A Bitcoin node running on a different device</li></ul>',
'variant-names': {
internal: 'Bitcoin Core',
external: 'External Node',
},
},
default: 'internal',
variants: {
internal: {},
external: {
'p2p-host': {
type: 'string',
name: 'Public Address',
description: 'The public address of your Bitcoin Core server',
nullable: false,
masked: false,
copyable: false,
},
'p2p-port': {
type: 'number',
name: 'P2P Port',
description:
'The port that your Bitcoin Core P2P server is bound to',
nullable: false,
range: '[0,65535]',
integral: true,
default: 8333,
},
},
},
},
},
},
advanced: {
name: 'Advanced',
type: 'object',
description: 'Advanced settings',
spec: {
rpcsettings: {
name: 'RPC Settings',
type: 'object',
description: 'rpc username and password',
warning:
'Adding RPC users gives them special permissions on your node.',
spec: {
rpcuser2: {
name: 'RPC Username',
type: 'string',
description: 'rpc username',
nullable: false,
default: 'defaultrpcusername',
pattern: '^[a-zA-Z]+$',
'pattern-description': 'must contain only letters.',
masked: false,
copyable: true,
},
rpcuser: {
name: 'RPC Username',
type: 'string',
description: 'rpc username',
nullable: false,
default: 'defaultrpcusername',
pattern: '^[a-zA-Z]+$',
'pattern-description': 'must contain only letters.',
masked: false,
copyable: true,
},
rpcpass: {
name: 'RPC User Password',
type: 'string',
description: 'rpc password',
nullable: false,
default: {
charset: 'a-z,A-Z,2-9',
len: 20,
},
masked: true,
copyable: true,
},
rpcpass2: {
name: 'RPC User Password',
type: 'string',
description: 'rpc password',
nullable: false,
default: {
charset: 'a-z,A-Z,2-9',
len: 20,
},
masked: true,
copyable: true,
},
},
},
},
},
testnet: {
name: 'Testnet',
type: 'boolean',
@@ -1449,6 +1556,29 @@ export module Mock {
},
},
external: {
'emergency-contact': {
name: 'Emergency Contact',
type: 'object',
description: 'The person to contact in case of emergency.',
spec: {
name: {
type: 'string',
name: 'Name',
nullable: false,
masked: false,
copyable: false,
pattern: '^[a-zA-Z]+$',
'pattern-description': 'Must contain only letters.',
},
email: {
type: 'string',
name: 'Email',
nullable: false,
masked: false,
copyable: true,
},
},
},
'public-domain': {
name: 'Public Domain',
type: 'string',
@@ -1520,8 +1650,8 @@ export module Mock {
copyable: false,
},
},
advanced: {
name: 'Advanced',
'more-advanced': {
name: 'More Advanced',
type: 'object',
description: 'Advanced settings',
spec: {
@@ -1726,8 +1856,7 @@ export module Mock {
rulemakers: [],
},
'bitcoin-node': {
type: 'external',
'public-domain': 'hello.com',
type: 'internal',
},
port: 20,
rpcallowip: undefined,

View File

@@ -21,13 +21,17 @@ export class ThemeSwitcherService extends BehaviorSubject<string> {
.watch$('ui', 'theme')
.pipe(take(1), filter(Boolean))
.subscribe(theme => {
this.next(theme)
this.updateTheme(theme)
})
}
override next(currentTheme: string): void {
this.embassyApi.setDbValue(['theme'], currentTheme)
this.windowRef.document.body.setAttribute('data-theme', currentTheme)
super.next(currentTheme)
override next(theme: string): void {
this.embassyApi.setDbValue(['theme'], theme)
this.updateTheme(theme)
}
private updateTheme(theme: string): void {
this.windowRef.document.body.setAttribute('data-theme', theme)
super.next(theme)
}
}