mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-04-01 21:13:09 +00:00
Subnav (#391)
* begin subnav implementation * implement subnav AND angular forms for comparison * unions working-ish, list of enums working * new form approach almost complete * finish new forms approach for action inputs and config * expandable list items and handlebars display * Config animation (#394) * config cammel * config animation Co-authored-by: Drew Ansbacher <drew.ansbacher@spiredigital.com> * improve server settings inputs, still needs work * delete all notifications, styling, and bugs * contracted by default Co-authored-by: Drew Ansbacher <drew.ansbacher@gmail.com> Co-authored-by: Drew Ansbacher <drew.ansbacher@spiredigital.com>
This commit is contained in:
committed by
Aiden McClelland
parent
4ea214f79e
commit
fd354b6cbe
@@ -7,7 +7,7 @@
|
||||
<ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item-divider *ngIf="spec.description || spec.changeWarning"></ion-item-divider>
|
||||
<ion-item-divider *ngIf="spec.description || spec['change-warning']"></ion-item-divider>
|
||||
</ng-container>
|
||||
<!-- description -->
|
||||
<ion-item *ngIf="spec.description">
|
||||
@@ -19,12 +19,12 @@
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<!-- warning -->
|
||||
<ion-item *ngIf="spec.changeWarning">
|
||||
<ion-item *ngIf="spec['change-warning']">
|
||||
<ion-label class="ion-text-wrap">
|
||||
<p>
|
||||
<ion-text color="warning">Warning!</ion-text>
|
||||
</p>
|
||||
<p [innerHTML]="spec.changeWarning | markdown"></p>
|
||||
<p [innerHTML]="spec['change-warning'] | markdown"></p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
@@ -0,0 +1,9 @@
|
||||
<ion-icon *ngIf="data.spec.description" class="help-icon" name="help-circle-outline" (click)="presentAlertDescription()"></ion-icon>
|
||||
<span> {{ data.spec.name }}</span>
|
||||
|
||||
<ion-text color="success" *ngIf="data.isNew"> (New)</ion-text>
|
||||
<ion-text color="warning" *ngIf="data.isEdited"> (Edited)</ion-text>
|
||||
|
||||
<span *ngIf="(['string', 'number'] | includes : data.spec.type) && !data.spec.nullable"> *</span>
|
||||
|
||||
<span *ngIf="data.spec.type === 'list' && Range.from(data.spec.range).min"> *</span>
|
||||
193
ui/src/app/components/form-object/form-object.component.html
Normal file
193
ui/src/app/components/form-object/form-object.component.html
Normal file
@@ -0,0 +1,193 @@
|
||||
<ion-item-group [formGroup]="formGroup">
|
||||
<div *ngFor="let entry of formGroup.controls | keyvalue : asIsOrder">
|
||||
<ng-container *ngIf="unionSpec && entry.key === unionSpec.tag.id">
|
||||
<p class="input-label">{{ unionSpec.tag.name }}</p>
|
||||
<ion-item color="dark">
|
||||
<ion-label>{{ unionSpec.tag.name }}</ion-label>
|
||||
<ion-select slot="end" placeholder="Select" [formControlName]="unionSpec.tag.id" [selectedText]="unionSpec.tag['variant-names'][entry.value.value]" (ionChange)="presentAlertChangeWarning(entry.key, unionSpec); 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>
|
||||
<ng-container *ngIf="objectSpec[entry.key] as spec">
|
||||
<!-- primitive -->
|
||||
<ng-container *ngIf="['string', 'number', 'boolean', 'enum'] | includes : spec.type">
|
||||
<!-- label -->
|
||||
<p class="input-label">
|
||||
<form-label [data]="{
|
||||
spec: spec,
|
||||
isNew: current && current[entry.key] === undefined,
|
||||
isEdited: entry.value.dirty
|
||||
}"></form-label>
|
||||
</p>
|
||||
<!-- string -->
|
||||
<ion-item color="dark" *ngIf="spec.type === 'string'">
|
||||
<ion-input [type]="spec.masked && !unmasked[entry.key] ? 'password' : 'text'" [placeholder]="'Enter ' + spec.name" [formControlName]="entry.key" (ionChange)="presentAlertChangeWarning(entry.key, spec)"></ion-input>
|
||||
<ion-button *ngIf="spec.masked" fill="clear" color="light" (click)="unmasked[entry.key] = !unmasked[entry.key]">
|
||||
<ion-icon slot="icon-only" [name]="unmasked[entry.key] ? 'eye-off-outline' : 'eye-outline'" size="small"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
<!-- number -->
|
||||
<ion-item color="dark" *ngIf="spec.type === 'number'">
|
||||
<ion-input type="tel" [placeholder]="'Enter ' + spec.name" [formControlName]="entry.key" (ionChange)="presentAlertChangeWarning(entry.key, spec)"></ion-input>
|
||||
<ion-note *ngIf="spec.units" slot="end" color="light" style="font-size: medium;">{{ spec.units }}</ion-note>
|
||||
</ion-item>
|
||||
<!-- boolean -->
|
||||
<ion-item color="dark" *ngIf="spec.type === 'boolean'">
|
||||
<ion-label>{{ spec.name }}</ion-label>
|
||||
<ion-toggle style="--background: var(--ion-color-step-600);" slot="end" color="light" [formControlName]="entry.key" (ionChange)="presentAlertChangeWarning(entry.key, spec)"></ion-toggle>
|
||||
</ion-item>
|
||||
<!-- enum -->
|
||||
<ion-item color="dark" *ngIf="spec.type === 'enum'">
|
||||
<ion-label>{{ spec.name }}</ion-label>
|
||||
<ion-select slot="end" placeholder="Select" [formControlName]="entry.key" [selectedText]="spec['value-names'][formGroup.get(entry.key).value]" (ionChange)="presentAlertChangeWarning(entry.key, spec)">
|
||||
<ion-select-option *ngFor="let option of spec.values" [value]="option">
|
||||
{{ spec['value-names'][option] }}
|
||||
</ion-select-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
<!-- object or union -->
|
||||
<ng-container *ngIf="spec.type === 'object' || spec.type ==='union'">
|
||||
<!-- label -->
|
||||
<ion-item-divider>
|
||||
<form-label [data]="{
|
||||
spec: spec,
|
||||
isNew: current && current[entry.key] === undefined,
|
||||
isEdited: entry.value.dirty
|
||||
}"></form-label>
|
||||
</ion-item-divider>
|
||||
<!-- body -->
|
||||
<div class="nested-wrapper">
|
||||
<form-object
|
||||
[objectSpec]="
|
||||
spec.type === 'union' ?
|
||||
spec.variants[entry.value.controls[spec.tag.id].value] :
|
||||
spec.spec"
|
||||
[formGroup]="entry.value"
|
||||
[current]="current ? current[key] : undefined"
|
||||
[unionSpec]="spec.type === 'union' ? spec : undefined"
|
||||
></form-object>
|
||||
</div>
|
||||
</ng-container>
|
||||
<!-- list (not enum) -->
|
||||
<ng-container *ngIf="spec.type === 'list' && spec.subtype !== 'enum'">
|
||||
<ng-container *ngIf="formGroup.get(entry.key) as formArr" [formArrayName]="entry.key">
|
||||
<!-- label -->
|
||||
<ion-item-divider>
|
||||
<form-label [data]="{
|
||||
spec: spec,
|
||||
isNew: current && current[entry.key] === undefined,
|
||||
isEdited: entry.value.dirty
|
||||
}"></form-label>
|
||||
<ion-button fill="clear" color="primary" slot="end" (click)="addListItem(entry.key)">
|
||||
<ion-icon slot="start" name="add"></ion-icon>
|
||||
Add
|
||||
</ion-button>
|
||||
</ion-item-divider>
|
||||
<!-- body -->
|
||||
<div class="nested-wrapper">
|
||||
<div
|
||||
*ngFor="let abstractControl of formArr.controls; let i = index;"
|
||||
class="ion-padding-top"
|
||||
>
|
||||
<!-- nested -->
|
||||
<ng-container *ngIf="spec.subtype === 'object' || spec.subtype === 'union'">
|
||||
<!-- nested label -->
|
||||
<ion-item-divider (click)="toggleExpand(entry.key, i)" style="cursor: pointer;">
|
||||
<form-label [data]="{
|
||||
spec: {
|
||||
name: objectListInfo[entry.key][i].displayAs || 'Entry ' + (i + 1)
|
||||
},
|
||||
isNew: false,
|
||||
isEdited: abstractControl.dirty
|
||||
}"></form-label>
|
||||
<ion-icon
|
||||
slot="end"
|
||||
name="chevron-up"
|
||||
[ngStyle]="{
|
||||
'transform': objectListInfo[entry.key][i].expanded ? 'rotate(0deg)' : 'rotate(180deg)',
|
||||
'transition': 'transform 0.4s ease-out'
|
||||
}"
|
||||
></ion-icon>
|
||||
</ion-item-divider>
|
||||
<!-- nested body -->
|
||||
<div
|
||||
[id]="entry.key"
|
||||
[ngStyle]="{
|
||||
'max-height': objectListInfo[entry.key][i].height,
|
||||
'overflow': 'hidden',
|
||||
'transition-property': 'max-height',
|
||||
'transition-duration': '.5s',
|
||||
'transition-delay': '.05s'
|
||||
}"
|
||||
>
|
||||
<!-- [hidden]="objectListInfo[entry.key][i].expanded ? false : true" -->
|
||||
|
||||
<form-object
|
||||
[objectSpec]="
|
||||
spec.subtype === 'union' ?
|
||||
spec.spec.variants[abstractControl.controls[spec.spec.tag.id].value] :
|
||||
spec.spec.spec"
|
||||
[formGroup]="abstractControl"
|
||||
[current]="current && current[entry.key] ? current[entry.key][i] : undefined"
|
||||
[unionSpec]="spec.subtype === 'union' ? spec.spec : undefined"
|
||||
></form-object>
|
||||
<div style="text-align: right; padding-top: 12px;">
|
||||
<ion-button fill="clear" (click)="presentAlertDelete(entry.key, i)" color="danger">
|
||||
<ion-icon slot="start" name="close"></ion-icon>
|
||||
Delete
|
||||
</ion-button>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<!-- string or number -->
|
||||
<ion-item-group *ngIf="spec.subtype === 'string' || spec.subtype === 'number'">
|
||||
<ion-item color="dark">
|
||||
<ion-input type="spec.spec.masked ? 'password' : 'text'" [placeholder]="'Enter ' + spec.name" [formControlName]="i"></ion-input>
|
||||
<ion-button slot="end" color="danger" (click)="presentAlertDelete(entry.key, i)">
|
||||
<ion-icon slot="icon-only" name="close"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
<ng-container *ngFor="let validation of formService.validationMessages[entry.key + '/' + i]">
|
||||
<p style="font-size: small;" *ngIf="abstractControl.hasError(validation.type) && (abstractControl.dirty || abstractControl.touched)">
|
||||
<ion-text color="danger">{{ validation.message }}</ion-text>
|
||||
</p>
|
||||
</ng-container>
|
||||
</ion-item-group>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<!-- list (enum) -->
|
||||
<ng-container *ngIf="spec.type === 'list' && spec.subtype === 'enum'">
|
||||
<ng-container *ngIf="formGroup.get(entry.key) as formArr" [formArrayName]="entry.key">
|
||||
<!-- label -->
|
||||
<p class="input-label">
|
||||
<form-label [data]="{
|
||||
spec: spec,
|
||||
isNew: current && current[entry.key] === undefined,
|
||||
isEdited: entry.value.dirty
|
||||
}"></form-label>
|
||||
</p>
|
||||
<!-- list -->
|
||||
<ion-item button detail="false" color="dark" (click)="presentModalEnumList(entry.key, spec, formArr.value)">
|
||||
<ion-label>
|
||||
<h2>{{ getEnumListDisplay(formArr.value, 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>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<div *ngFor="let validation of formService.validationMessages[entry.key]">
|
||||
<p class="validation-error" *ngIf="formGroup.get(entry.key).hasError(validation.type)">
|
||||
<ion-text *ngIf="(formGroup.get(entry.key).dirty || formGroup.get(entry.key).touched)" color="danger">{{ spec.name }}: {{ validation.message }}</ion-text>
|
||||
</p>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ion-item-group>
|
||||
@@ -0,0 +1,27 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { FormObjectComponent, FormLabelComponent } from './form-object.component'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
import { EnumListPageModule } from 'src/app/modals/enum-list/enum-list.module'
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
FormObjectComponent,
|
||||
FormLabelComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
SharingModule,
|
||||
EnumListPageModule,
|
||||
],
|
||||
exports: [
|
||||
FormObjectComponent,
|
||||
FormLabelComponent,
|
||||
],
|
||||
})
|
||||
export class FormObjectComponentModule { }
|
||||
41
ui/src/app/components/form-object/form-object.component.scss
Normal file
41
ui/src/app/components/form-object/form-object.component.scss
Normal file
@@ -0,0 +1,41 @@
|
||||
.help-icon {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
padding-bottom: 2px;
|
||||
font-size: 18px;
|
||||
color: var(--ion-color-dark);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
ion-input {
|
||||
font-weight: 500;
|
||||
--placeholder-font-weight: 400;
|
||||
}
|
||||
|
||||
ion-item-divider {
|
||||
text-transform: unset;
|
||||
--padding-start: 0;
|
||||
border-bottom: 1px solid var(--ion-item-border-color, var(--ion-border-color, var(--ion-color-step-150, rgba(0, 0, 0, 0.13))))
|
||||
}
|
||||
|
||||
.input-label {
|
||||
// padding-top: 10px;
|
||||
margin-bottom: 5px;
|
||||
font-size: medium;
|
||||
font-weight: bold;
|
||||
* {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.nested-wrapper {
|
||||
padding: 0 0 30px 30px;
|
||||
// border-bottom: 1px solid var(--ion-item-border-color, var(--ion-border-color, var(--ion-color-step-150, rgba(0, 0, 0, 0.13))))
|
||||
}
|
||||
|
||||
.validation-error {
|
||||
p {
|
||||
font-size: small;
|
||||
}
|
||||
}
|
||||
223
ui/src/app/components/form-object/form-object.component.ts
Normal file
223
ui/src/app/components/form-object/form-object.component.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
import { Component, Input, SimpleChange } from '@angular/core'
|
||||
import { FormArray, FormGroup } from '@angular/forms'
|
||||
import { AlertController, ModalController } from '@ionic/angular'
|
||||
import { ConfigSpec, ListValueSpecOf, ValueSpec, 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 * as handlebars from 'handlebars'
|
||||
import { pauseFor } from 'src/app/util/misc.util'
|
||||
|
||||
@Component({
|
||||
selector: 'form-object',
|
||||
templateUrl: './form-object.component.html',
|
||||
styleUrls: ['./form-object.component.scss'],
|
||||
})
|
||||
export class FormObjectComponent {
|
||||
@Input() objectSpec: ConfigSpec
|
||||
@Input() formGroup: FormGroup
|
||||
@Input() unionSpec: ValueSpecUnion
|
||||
@Input() current: { [key: string]: any }
|
||||
@Input() showEdited: boolean = false
|
||||
warningAck: { [key: string]: boolean } = { }
|
||||
unmasked: { [key: string]: boolean } = { }
|
||||
// @TODO for when we want to expand/collapse normal objects/union in addition to list ones
|
||||
// objectExpanded: { [key: string]: boolean } = { }
|
||||
objectListInfo: { [key: string]: { expanded: boolean, height: string, displayAs: string }[] } = { }
|
||||
Object = Object
|
||||
|
||||
constructor (
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly formService: FormService,
|
||||
) { }
|
||||
|
||||
ngOnChanges (changes: { [propName: string]: SimpleChange }) {
|
||||
// @TODO figure out why changes are being triggered so often. If too heavy, switch to ngOnInit and figure out another way to manually reset defaults is executed. Needed because otherwise ObjectListInfo won't be accurate.
|
||||
|
||||
// if ( changes['current'] && changes['current'].previousValue != changes['current'].currentValue ) {
|
||||
// console.log('CURRENT')
|
||||
// }
|
||||
// if ( changes['formGroup'] && changes['formGroup'].previousValue != changes['formGroup'].currentValue ) {
|
||||
// console.log('FORM GROUP')
|
||||
// }
|
||||
// if ( changes['objectSpec'] && changes['objectSpec'].previousValue != changes['objectSpec'].currentValue ) {
|
||||
// console.log('OBJECT SPEC')
|
||||
// }
|
||||
// Lists are automatically expanded, but their members are not
|
||||
Object.keys(this.objectSpec).forEach(key => {
|
||||
const spec = this.objectSpec[key]
|
||||
if (spec.type === 'list' && ['object', 'union'].includes(spec.subtype)) {
|
||||
this.objectListInfo[key] = [];
|
||||
(this.formGroup.get(key).value as any[]).forEach((obj, index) => {
|
||||
const displayAs = (spec.spec as ListValueSpecOf<'object'>)['display-as']
|
||||
this.objectListInfo[key][index] = {
|
||||
expanded: false,
|
||||
height: '0px',
|
||||
displayAs: displayAs ? handlebars.compile(displayAs)(obj) : '',
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
getEnumListDisplay (arr: string[], spec: ListValueSpecOf<'enum'>): string {
|
||||
return arr.map((v: string) => spec['value-names'][v]).join(', ')
|
||||
}
|
||||
|
||||
updateUnion (e: any): void {
|
||||
Object.keys(this.formGroup.controls).forEach(control => {
|
||||
if (control === 'type') 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 === 'type') return
|
||||
this.formGroup.addControl(control, unionGroup.controls[control])
|
||||
})
|
||||
}
|
||||
|
||||
addListItem (key: string, markDirty = true, val?: string): void {
|
||||
const arr = this.formGroup.get(key) as FormArray
|
||||
if (markDirty) arr.markAsDirty()
|
||||
// const validators = this.formService.getListItemValidators(this.objectSpec[key] as ValueSpecList, key, arr.length)
|
||||
// arr.push(new FormControl(value, validators))
|
||||
const listSpec = this.objectSpec[key] as ValueSpecList
|
||||
const newItem = this.formService.getListItem(key, arr.length, listSpec, val)
|
||||
newItem.markAllAsTouched()
|
||||
arr.insert(0, newItem)
|
||||
if (['object', 'union'].includes(listSpec.subtype)) {
|
||||
const displayAs = (listSpec.spec as ListValueSpecOf<'object'>)['display-as']
|
||||
this.objectListInfo[key].push({
|
||||
height: '0px',
|
||||
expanded: true,
|
||||
displayAs: displayAs ? handlebars.compile(displayAs)(newItem.value) : '',
|
||||
})
|
||||
}
|
||||
pauseFor(200).then(() => {
|
||||
const index = this.objectListInfo[key].length - 1
|
||||
this.objectListInfo[key][index].height = this.getDocSize(key)
|
||||
})
|
||||
}
|
||||
|
||||
toggleExpand (key: string, i: number) {
|
||||
this.objectListInfo[key][i].expanded = !this.objectListInfo[key][i].expanded
|
||||
this.objectListInfo[key][i].height = this.objectListInfo[key][i].expanded ? this.getDocSize(key) : '0px'
|
||||
|
||||
}
|
||||
|
||||
async presentModalEnumList (key: string, spec: ValueSpecListOf<'enum'>, current: string[]) {
|
||||
const modal = await this.modalCtrl.create({
|
||||
componentProps: {
|
||||
key,
|
||||
spec,
|
||||
current,
|
||||
},
|
||||
component: EnumListPage,
|
||||
})
|
||||
|
||||
modal.onWillDismiss().then(res => {
|
||||
const data = res.data
|
||||
if (!data) return
|
||||
this.updateEnumList(key, current, data)
|
||||
})
|
||||
|
||||
await modal.present()
|
||||
}
|
||||
|
||||
async presentAlertChangeWarning (key: string, spec: ValueSpec) {
|
||||
if (!spec['change-warning'] || this.warningAck[key]) return
|
||||
this.warningAck[key] = true
|
||||
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Warning',
|
||||
subHeader: `Editing ${spec.name} has consequences:`,
|
||||
message: spec['change-warning'],
|
||||
buttons: ['Ok'],
|
||||
})
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
async presentAlertDelete (key: string, index: number) {
|
||||
const alert = await this.alertCtrl.create({
|
||||
backdropDismiss: false,
|
||||
header: 'Confirm',
|
||||
message: 'Are you sure you want to delete this entry?',
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
role: 'cancel',
|
||||
},
|
||||
{
|
||||
text: 'Delete',
|
||||
handler: () => {
|
||||
this.deleteListItem(key, index)
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
private deleteListItem (key: string, index: number, markDirty = true): void {
|
||||
this.objectListInfo[key][index].height = '0px'
|
||||
const arr = this.formGroup.get(key) as FormArray
|
||||
if (markDirty) arr.markAsDirty()
|
||||
pauseFor(500).then(() => {
|
||||
this.objectListInfo[key].splice(index, 1)
|
||||
arr.removeAt(index)
|
||||
})
|
||||
}
|
||||
|
||||
private updateEnumList (key: string, current: string[], updated: string[]) {
|
||||
this.formGroup.get(key).markAsDirty()
|
||||
|
||||
let deleted = current.filter(x => !updated.includes(x))
|
||||
deleted.forEach((_, index) => this.deleteListItem(key, index, false))
|
||||
|
||||
let added = updated.filter(x => !current.includes(x))
|
||||
added.forEach(val => this.addListItem(key, false, val))
|
||||
}
|
||||
|
||||
getDocSize (selected: string) {
|
||||
const element = document.getElementById(selected)
|
||||
return `${element.scrollHeight}px`
|
||||
}
|
||||
|
||||
asIsOrder () {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
interface HeaderData {
|
||||
spec: ValueSpec
|
||||
isEdited: boolean
|
||||
isNew: boolean
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'form-label',
|
||||
templateUrl: './form-label.component.html',
|
||||
styleUrls: ['./form-object.component.scss'],
|
||||
})
|
||||
export class FormLabelComponent {
|
||||
Range = Range
|
||||
@Input() data: HeaderData
|
||||
|
||||
constructor (
|
||||
private readonly alertCtrl: AlertController,
|
||||
) { }
|
||||
|
||||
async presentAlertDescription () {
|
||||
const { name, description } = this.data.spec
|
||||
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: name,
|
||||
message: description,
|
||||
buttons: ['Ok'],
|
||||
})
|
||||
await alert.present()
|
||||
}
|
||||
}
|
||||
@@ -39,11 +39,11 @@
|
||||
<!-- cancel button if loading/not loading -->
|
||||
<ion-button slot="start" *ngIf="(currentSlide.loading$ | async) && currentBottomBar.cancel.whileLoading as cancel" (click)="transitions.cancel()" class="toolbar-button" fill="outline">
|
||||
<ion-text *ngIf="cancel.text" [class.smaller-text]="cancel.text > 16">{{ cancel.text }}</ion-text>
|
||||
<ion-icon *ngIf="!cancel.text" name="close-outline"></ion-icon>
|
||||
<ion-icon *ngIf="!cancel.text" name="close"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button slot="start" *ngIf="!(currentSlide.loading$ | async) && currentBottomBar.cancel.afterLoading as cancel" (click)="transitions.cancel()" class="toolbar-button" fill="outline">
|
||||
<ion-text *ngIf="cancel.text" [class.smaller-text]="cancel.text > 16">{{ cancel.text }}</ion-text>
|
||||
<ion-icon *ngIf="!cancel.text" name="close-outline"></ion-icon>
|
||||
<ion-icon *ngIf="!cancel.text" name="close"></ion-icon>
|
||||
</ion-button>
|
||||
|
||||
<!-- next/finish buttons -->
|
||||
|
||||
@@ -139,7 +139,7 @@ export class WizardBaker {
|
||||
},
|
||||
},
|
||||
bottomBar: {
|
||||
cancel: { afterLoading: { text: 'Cancel' } }, next: 'Update OS',
|
||||
cancel: { afterLoading: { text: 'Cancel' } }, next: 'Begin Update',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<div *ngFor="let keyval of (spec.type === 'object' ? spec.spec : spec.variants[value[spec.tag.id]]) | keyvalue: asIsOrder">
|
||||
<ion-item-group>
|
||||
<object-config-item
|
||||
*ngFor="let keyval of (spec.type === 'object' ? spec.spec : spec.variants[value[spec.tag.id]]) | keyvalue: asIsOrder"
|
||||
[key]="keyval.key"
|
||||
[spec]="keyval.value"
|
||||
[value]="value[keyval.key]"
|
||||
@@ -7,4 +8,4 @@
|
||||
(onClick)="handleClick(keyval.key)"
|
||||
[class.add-margin]="keyval.key === 'advanced'"
|
||||
></object-config-item>
|
||||
</div>
|
||||
</ion-item-group>
|
||||
@@ -10,28 +10,11 @@
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.status-icon{
|
||||
// width: 2%;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.bright {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.invalid {
|
||||
color: var(--ion-color-danger) !important;
|
||||
}
|
||||
|
||||
.organizer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.name {
|
||||
text-decoration: underline;
|
||||
}
|
||||
@@ -1,17 +1,17 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core'
|
||||
import { Annotation, Annotations } from '../../pkg-config/config-utilities'
|
||||
import { TrackingModalController } from 'src/app/services/tracking-modal-controller.service'
|
||||
import { ConfigCursor } from 'src/app/pkg-config/config-cursor'
|
||||
import { ModalPresentable } from 'src/app/pkg-config/modal-presentable'
|
||||
import { ValueSpecOf, ValueSpec } from 'src/app/pkg-config/config-types'
|
||||
import { MaskPipe } from 'src/app/pipes/mask.pipe'
|
||||
import { IonNav } from '@ionic/angular'
|
||||
import { SubNavService } from 'src/app/services/sub-nav.service'
|
||||
|
||||
@Component({
|
||||
selector: 'object-config',
|
||||
templateUrl: './object-config.component.html',
|
||||
styleUrls: ['./object-config.component.scss'],
|
||||
})
|
||||
export class ObjectConfigComponent extends ModalPresentable {
|
||||
export class ObjectConfigComponent {
|
||||
@Input() cursor: ConfigCursor<'object' | 'union'>
|
||||
@Output() onEdit = new EventEmitter<boolean>()
|
||||
spec: ValueSpecOf<'object' | 'union'>
|
||||
@@ -19,10 +19,9 @@ export class ObjectConfigComponent extends ModalPresentable {
|
||||
annotations: Annotations<'object' | 'union'>
|
||||
|
||||
constructor (
|
||||
trackingModalCtrl: TrackingModalController,
|
||||
) {
|
||||
super(trackingModalCtrl)
|
||||
}
|
||||
private readonly subNav: SubNavService,
|
||||
private readonly nav: IonNav,
|
||||
) { }
|
||||
|
||||
ngOnInit () {
|
||||
this.spec = this.cursor.spec()
|
||||
@@ -33,11 +32,7 @@ export class ObjectConfigComponent extends ModalPresentable {
|
||||
async handleClick (key: string) {
|
||||
const nextCursor = this.cursor.seekNext(key)
|
||||
nextCursor.createFirstEntryForList()
|
||||
|
||||
await this.presentModal(nextCursor, () => {
|
||||
this.onEdit.emit(true)
|
||||
this.annotations = this.cursor.getAnnotations()
|
||||
})
|
||||
this.subNav.push(key, nextCursor, this.nav)
|
||||
}
|
||||
|
||||
asIsOrder () {
|
||||
@@ -50,7 +45,6 @@ export class ObjectConfigComponent extends ModalPresentable {
|
||||
templateUrl: './object-config-item.component.html',
|
||||
styleUrls: ['./object-config.component.scss'],
|
||||
})
|
||||
|
||||
export class ObjectConfigItemComponent {
|
||||
@Input() key: string
|
||||
@Input() spec: ValueSpec
|
||||
@@ -84,7 +78,7 @@ export class ObjectConfigItemComponent {
|
||||
}
|
||||
break
|
||||
case 'enum':
|
||||
this.displayValue = this.spec.valueNames[this.value]
|
||||
this.displayValue = this.spec['value-names'][this.value]
|
||||
break
|
||||
case 'pointer':
|
||||
this.displayValue = 'System Defined'
|
||||
|
||||
@@ -3,7 +3,6 @@ import { CommonModule } from '@angular/common'
|
||||
import { PwaBackComponent } from './pwa-back.component'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@@ -13,7 +12,6 @@ import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild([]),
|
||||
SharingModule,
|
||||
],
|
||||
exports: [PwaBackComponent],
|
||||
})
|
||||
|
||||
18
ui/src/app/components/sub-nav/sub-nav.component.html
Normal file
18
ui/src/app/components/sub-nav/sub-nav.component.html
Normal file
@@ -0,0 +1,18 @@
|
||||
<ion-header *ngIf="subNav.path.length as length">
|
||||
<ion-toolbar class="subheader" color="dark">
|
||||
<ion-buttons slot="start" *ngIf="length > 1">
|
||||
<ion-button color="light" (click)="subNav.pop(nav)">
|
||||
<ion-icon slot="icon-only" name="arrow-back-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
<ion-title>
|
||||
<span *ngFor="let segment of subNav.path; let i = index">
|
||||
/
|
||||
<span *ngIf="i === length -1"> {{ segment }}</span>
|
||||
<a *ngIf="i !== length -1" style="cursor: pointer;" (click)="subNav.popTo(i, nav)"> {{ segment }}</a>
|
||||
</span>
|
||||
</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-nav [root]="rootPage" [rootParams]="rootParams"></ion-nav>
|
||||
16
ui/src/app/components/sub-nav/sub-nav.component.module.ts
Normal file
16
ui/src/app/components/sub-nav/sub-nav.component.module.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { SubNavComponent } from './sub-nav.component'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
SubNavComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
],
|
||||
exports: [SubNavComponent],
|
||||
})
|
||||
export class SubNavComponentModule { }
|
||||
23
ui/src/app/components/sub-nav/sub-nav.component.ts
Normal file
23
ui/src/app/components/sub-nav/sub-nav.component.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Component, Input, ViewChild } from '@angular/core'
|
||||
import { IonNav } from '@ionic/angular'
|
||||
import { SubNavService } from 'src/app/services/sub-nav.service'
|
||||
|
||||
@Component({
|
||||
selector: 'sub-nav',
|
||||
templateUrl: './sub-nav.component.html',
|
||||
styleUrls: ['./sub-nav.component.scss'],
|
||||
})
|
||||
export class SubNavComponent {
|
||||
@Input() path: string
|
||||
@Input() rootPage: any
|
||||
@Input() rootParams: { [key: string]: any }
|
||||
@ViewChild(IonNav) nav: IonNav
|
||||
|
||||
constructor (
|
||||
public readonly subNav: SubNavService,
|
||||
) { }
|
||||
|
||||
ngOnInit () {
|
||||
this.subNav.path = [this.path]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user