error flattening, remain to details, and delete old config stuff

This commit is contained in:
Matt Hill
2021-08-09 13:39:30 -06:00
committed by Aiden McClelland
parent 82928fe3ba
commit a6cf6edcea
26 changed files with 42 additions and 754 deletions

View File

@@ -1,19 +0,0 @@
import { AppConfigObjectPage } from './app-config-object/app-config-object.page'
import { AppConfigListPage } from './app-config-list/app-config-list.page'
import { AppConfigUnionPage } from './app-config-union/app-config-union.page'
import { AppConfigValuePage } from './app-config-value/app-config-value.page'
import { Type } from '@angular/core'
import { ValueType } from 'src/app/pkg-config/config-types'
export const appConfigComponents: AppConfigComponentMapping = {
'string': AppConfigValuePage,
'number': AppConfigValuePage,
'enum': AppConfigValuePage,
'boolean': AppConfigValuePage,
'list': AppConfigListPage,
'object': AppConfigObjectPage,
'union': AppConfigUnionPage,
'pointer': undefined,
}
export type AppConfigComponentMapping = { [k in ValueType]: Type<any> }

View File

@@ -1,21 +0,0 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { AppConfigListPage } from './app-config-list.page'
import { SharingModule } from 'src/app/modules/sharing.module'
import { ConfigHeaderComponentModule } from 'src/app/components/config-header/config-header.component.module'
import { FormsModule } from '@angular/forms'
@NgModule({
declarations: [AppConfigListPage],
imports: [
CommonModule,
IonicModule,
SharingModule,
FormsModule,
ConfigHeaderComponentModule,
],
entryComponents: [AppConfigListPage],
exports: [AppConfigListPage],
})
export class AppConfigListPageModule { }

View File

@@ -1,53 +0,0 @@
<ion-content class="subheader-padding">
<config-header [spec]="spec" [error]="error"></config-header>
<ion-button *ngIf="spec.subtype !== 'enum'" (click)="createOrEdit()">
<ion-icon name="add" color="primary"></ion-icon>
</ion-button>
<!-- enum list -->
<ion-item-group *ngIf="spec.subtype === 'enum'">
<ion-item-divider class="borderless"></ion-item-divider>
<ion-item-divider>
{{ value.length }} selected
<span *ngIf="min">&nbsp;(min: {{ min }})</span>
<span *ngIf="max">&nbsp;(max: {{ max }})</span>
<ion-button slot="end" fill="clear" color="primary" (click)="toggleSelectAll()">{{ selectAll ? 'All' : 'None' }}</ion-button>
</ion-item-divider>
<ion-item *ngFor="let option of options">
<ion-label>{{ option.value }}</ion-label>
<ion-checkbox slot="end" [(ngModel)]="option.checked" (click)="toggleSelected(option.value)"></ion-checkbox>
</ion-item>
</ion-item-group>
<!-- not enum list -->
<div *ngIf="spec.subtype !== 'enum'">
<ion-item-divider class="borderless"></ion-item-divider>
<ion-item-group>
<ion-item-divider style="font-size: small; color: var(--ion-color-medium);">
{{ value.length }}&nbsp;
<span *ngIf="value.length === 1">Entry</span>
<span *ngIf="value.length !== 1">Entries</span>
<span *ngIf="min">&nbsp;(min: {{ min }})</span>
<span *ngIf="max">&nbsp;(max: {{ max }})</span>
</ion-item-divider>
<div *ngFor="let v of value; index as i;">
<ion-item button detail="false" (click)="createOrEdit(i)">
<ion-icon size="small" slot="start" *ngIf="!annotations.members[i] || (annotations.members[i] | annotationStatus: 'NoChange')" style="margin-right: 15px; color: rgba(0,0,0,0); background: radial-gradient(#2a4e8970, #2a4e8970 35%, transparent 35%, transparent);" name="ellipse"></ion-icon>
<ion-icon size="small" slot="start" *ngIf="!annotations.members[i] || (annotations.members[i] | annotationStatus: 'Added')" style="margin-right: 15px; color: rgba(0,0,0,0); background: radial-gradient(#2a4e8970, #2a4e8970 35%, transparent 35%, transparent);" name="ellipse"></ion-icon>
<ion-icon size="small" slot="start" *ngIf="annotations.members[i] && (annotations.members[i] | annotationStatus: 'Edited')" style="margin-right: 15px" color="primary" name="ellipse"></ion-icon>
<ion-icon size="small" slot="start" *ngIf="annotations.members[i] && (annotations.members[i] | annotationStatus: 'Invalid')" style="margin-right: 15px" color="danger" name="warning-outline"></ion-icon>
<ion-label>{{ valueString[i] }}</ion-label>
<ion-button slot="end" fill="clear" (click)="presentAlertDelete(i, $event)">
<ion-icon slot="icon-only" name="close"></ion-icon>
</ion-button>
</ion-item>
</div>
</ion-item-group>
</div>
</ion-content>

View File

@@ -1,140 +0,0 @@
import { Component, Input } from '@angular/core'
import { AlertController, IonNav } from '@ionic/angular'
import { Annotations, Range } from '../../pkg-config/config-utilities'
import { ConfigCursor } from 'src/app/pkg-config/config-cursor'
import { ValueSpecList, isValueSpecListOf } from 'src/app/pkg-config/config-types'
import { SubNavService } from 'src/app/services/sub-nav.service'
@Component({
selector: 'app-config-list',
templateUrl: './app-config-list.page.html',
styleUrls: ['./app-config-list.page.scss'],
})
export class AppConfigListPage {
@Input() cursor: ConfigCursor<'list'>
spec: ValueSpecList
value: string[] | number[] | object[]
valueString: string[]
annotations: Annotations<'list'>
// enum only
options: { value: string, checked: boolean }[] = []
selectAll = true
min: number | undefined
max: number | undefined
minMessage: string
maxMessage: string
error: string
constructor (
private readonly alertCtrl: AlertController,
private readonly subNav: SubNavService,
private readonly nav: IonNav,
) { }
ngOnInit () {
this.spec = this.cursor.spec()
this.value = this.cursor.config()
const range = Range.from(this.spec.range)
this.min = range.integralMin()
this.max = range.integralMax()
this.minMessage = `The minimum number of ${this.cursor.key()} is ${this.min}.`
this.maxMessage = `The maximum number of ${this.cursor.key()} is ${this.max}.`
// enum list only
if (isValueSpecListOf(this.spec, 'enum')) {
for (let val of this.spec.spec.values) {
this.options.push({
value: val,
checked: (this.value as string[]).includes(val),
})
}
}
this.updateCaches()
}
// enum only
toggleSelectAll () {
if (!isValueSpecListOf(this.spec, 'enum')) { throw new Error('unreachable') }
this.value.length = 0
if (this.selectAll) {
for (let v of this.spec.spec.values) {
(this.value as string[]).push(v)
}
for (let option of this.options) {
option.checked = true
}
} else {
for (let option of this.options) {
option.checked = false
}
}
this.updateCaches()
}
// enum only
async toggleSelected (value: string) {
const index = (this.value as string[]).indexOf(value)
// if present, delete
if (index > -1) {
(this.value as string[]).splice(index, 1)
// if not present, add
} else {
(this.value as string[]).push(value)
}
this.updateCaches()
}
async createOrEdit (index?: number) {
const nextCursor = this.cursor.seekNext(index === undefined ? this.value.length : index)
nextCursor.createFirstEntryForList()
this.subNav.push(String(index), nextCursor, this.nav)
}
async presentAlertDelete (key: number, e: Event) {
e.stopPropagation()
const alert = await this.alertCtrl.create({
backdropDismiss: false,
header: 'Caution',
message: `Are you sure you want to delete this entry?`,
buttons: [
{
text: 'Cancel',
role: 'cancel',
},
{
text: 'Delete',
handler: () => {
if (typeof key === 'number') {
(this.value as any[]).splice(key, 1)
} else {
delete this.value[key]
}
this.updateCaches()
},
},
],
})
await alert.present()
}
asIsOrder () {
return 0
}
private updateCaches () {
if (isValueSpecListOf(this.spec, 'enum')) {
this.selectAll = this.value.length !== this.spec.spec.values.length
}
this.error = this.cursor.checkInvalid()
this.annotations = this.cursor.getAnnotations()
this.valueString = (this.value as any[]).map((_, idx) => this.cursor.seekNext(idx).toString())
}
}

View File

@@ -1,19 +0,0 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { AppConfigObjectPage } from './app-config-object.page'
import { ObjectConfigComponentModule } from 'src/app/components/object-config/object-config.component.module'
import { ConfigHeaderComponentModule } from 'src/app/components/config-header/config-header.component.module'
@NgModule({
declarations: [AppConfigObjectPage],
imports: [
CommonModule,
IonicModule,
ObjectConfigComponentModule,
ConfigHeaderComponentModule,
],
entryComponents: [AppConfigObjectPage],
exports: [AppConfigObjectPage],
})
export class AppConfigObjectPageModule { }

View File

@@ -1,4 +0,0 @@
<ion-content class="subheader-padding">
<config-header [spec]="spec" [error]="error"></config-header>
<object-config [cursor]="cursor" (onEdit)="handleObjectEdit()" (onClick)="updatePath($event)"></object-config>
</ion-content>

View File

@@ -1,56 +0,0 @@
import { Component, Input } from '@angular/core'
import { ModalController, AlertController } from '@ionic/angular'
import { ConfigCursor } from 'src/app/pkg-config/config-cursor'
import { ValueSpecObject } from 'src/app/pkg-config/config-types'
@Component({
selector: 'app-config-object',
templateUrl: './app-config-object.page.html',
styleUrls: ['./app-config-object.page.scss'],
})
export class AppConfigObjectPage {
@Input() cursor: ConfigCursor<'object'>
spec: ValueSpecObject
value: object
error: string
constructor (
private readonly modalCtrl: ModalController,
private readonly alertCtrl: AlertController,
) { }
ngOnInit () {
this.spec = this.cursor.spec()
this.value = this.cursor.config()
this.error = this.cursor.checkInvalid()
}
async dismiss (nullify = false) {
this.modalCtrl.dismiss(nullify ? null : this.value)
}
async presentAlertDestroy () {
const alert = await this.alertCtrl.create({
backdropDismiss: false,
header: 'Caution',
message: 'Are you sure you want to delete this record?',
buttons: [
{
text: 'Cancel',
role: 'cancel',
},
{
text: 'Delete',
handler: () => {
this.dismiss(true)
},
},
],
})
await alert.present()
}
handleObjectEdit () {
this.error = this.cursor.checkInvalid()
}
}

View File

@@ -1,22 +0,0 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { AppConfigUnionPage } from './app-config-union.page'
import { ObjectConfigComponentModule } from 'src/app/components/object-config/object-config.component.module'
import { FormsModule } from '@angular/forms'
import { ConfigHeaderComponentModule } from 'src/app/components/config-header/config-header.component.module'
@NgModule({
declarations: [AppConfigUnionPage],
imports: [
CommonModule,
IonicModule,
FormsModule,
ObjectConfigComponentModule,
ConfigHeaderComponentModule,
],
entryComponents: [AppConfigUnionPage],
exports: [AppConfigUnionPage],
})
export class AppConfigUnionPageModule { }

View File

@@ -1,27 +0,0 @@
<ion-content class="subheader-padding">
<config-header [spec]="spec" [error]="error"></config-header>
<!-- union -->
<ion-item-group>
<ion-item-divider></ion-item-divider>
<ion-item>
<ion-icon size="small" slot="start" *ngIf="!edited"
style="margin-right: 15px; color: rgba(0,0,0,0); background: radial-gradient(#2a4e8970, #2a4e8970 35%, transparent 35%, transparent);"
name="ellipse"></ion-icon>
<ion-icon size="small" slot="start" *ngIf="edited" style="margin-right: 15px" color="primary" name="ellipse">
</ion-icon>
<ion-label>{{ spec.tag.name }}</ion-label>
<ion-select slot="end" [interfaceOptions]="setSelectOptions()" placeholder="Select One"
[(ngModel)]="value[spec.tag.id]" [selectedText]="spec.tag['variant-names'][value[spec.tag.id]]"
(ngModelChange)="handleUnionChange()">
<ion-select-option *ngFor="let option of spec.variants | keyvalue: asIsOrder" [value]="option.key">
{{ spec.tag['variant-names'][option.key] }}
<span *ngIf="option.key === spec.default"> (default)</span>
</ion-select-option>
</ion-select>
</ion-item>
<object-config [cursor]="cursor" (onEdit)="handleObjectEdit()"></object-config>
</ion-item-group>
</ion-content>

View File

@@ -1,62 +0,0 @@
import { Component, Input, ViewChild } from '@angular/core'
import { ModalController } from '@ionic/angular'
import { ConfigCursor } from 'src/app/pkg-config/config-cursor'
import { ValueSpecUnion } from 'src/app/pkg-config/config-types'
import { ObjectConfigComponent } from 'src/app/components/object-config/object-config.component'
import { mapUnionSpec } from '../../pkg-config/config-utilities'
@Component({
selector: 'app-config-union',
templateUrl: './app-config-union.page.html',
styleUrls: ['./app-config-union.page.scss'],
})
export class AppConfigUnionPage {
@Input() cursor: ConfigCursor<'union'>
@ViewChild(ObjectConfigComponent)
objectConfig: ObjectConfigComponent
spec: ValueSpecUnion
value: object
error: string
edited: boolean
constructor (
private readonly modalCtrl: ModalController,
) { }
ngOnInit () {
this.spec = this.cursor.spec()
this.value = this.cursor.config()
this.error = this.cursor.checkInvalid()
this.edited = this.cursor.seekNext(this.spec.tag.id).isEdited()
}
async dismiss () {
this.modalCtrl.dismiss(this.value)
}
async handleUnionChange () {
this.value = mapUnionSpec(this.spec, this.value)
this.objectConfig.annotations = this.objectConfig.cursor.getAnnotations()
this.error = this.cursor.checkInvalid()
this.edited = this.cursor.seekNext(this.spec.tag.id).isEdited()
}
setSelectOptions () {
return {
header: this.spec.tag.name,
subHeader: this.spec['change-warning'] ? 'Warning!' : undefined,
message: this.spec['change-warning'] ? `${this.spec['change-warning']}` : undefined,
cssClass: 'select-change-warning',
}
}
handleObjectEdit () {
this.error = this.cursor.checkInvalid()
}
asIsOrder () {
return 0
}
}

View File

@@ -1,19 +0,0 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { FormsModule } from '@angular/forms'
import { IonicModule } from '@ionic/angular'
import { AppConfigValuePage } from './app-config-value.page'
import { ConfigHeaderComponentModule } from 'src/app/components/config-header/config-header.component.module'
@NgModule({
declarations: [AppConfigValuePage],
imports: [
CommonModule,
FormsModule,
IonicModule,
ConfigHeaderComponentModule,
],
entryComponents: [AppConfigValuePage],
exports: [AppConfigValuePage],
})
export class AppConfigValuePageModule { }

View File

@@ -1,63 +0,0 @@
<ion-content class="subheader-padding">
<config-header [spec]="spec" [error]="error"></config-header>
<ion-item-group>
<ion-item-divider>
<ion-button *ngIf="spec.type === 'string' && spec.copyable" style="padding-right: 12px;" size="small" slot="end" fill="clear" (click)="copy()">
<ion-icon slot="icon-only" name="copy-outline" size="small"></ion-icon>
</ion-button>
</ion-item-divider>
<!-- string -->
<ion-item *ngIf="spec.type === 'string'">
<ion-input [type]="spec.masked && !unmasked ? 'password' : 'text'" placeholder="Enter value" [(ngModel)]="value" (ngModelChange)="handleInput()"></ion-input>
<div slot="end">
<ion-button *ngIf="spec.masked" fill="clear" [color]="unmasked ? 'danger' : 'primary'" (click)="toggleMask()">
<ion-icon slot="icon-only" [name]="unmasked ? 'eye-off-outline' : 'eye-outline'" size="small"></ion-icon>
</ion-button>
<ion-button *ngIf="value && spec.nullable" fill="clear" (click)="clear()">
<ion-icon slot="icon-only" name="close" size="small"></ion-icon>
</ion-button>
</div>
</ion-item>
<!-- number -->
<ion-item *ngIf="spec.type === 'number'">
<ion-input type="tel" placeholder="Enter value" [(ngModel)]="value" (ngModelChange)="handleInput()"></ion-input>
<span slot="end" *ngIf="spec.units"><ion-text>{{ spec.units }}</ion-text></span>
<ion-button *ngIf="value && spec.nullable" slot="end" fill="clear" (click)="clear()">
<ion-icon slot="icon-only" name="close" size="small"></ion-icon>
</ion-button>
</ion-item>
<!-- boolean -->
<ion-item *ngIf="spec.type === 'boolean'">
<ion-label>{{ spec.name }}</ion-label>
<ion-toggle slot="end" [(ngModel)]="value" (ngModelChange)="edited = true"></ion-toggle>
</ion-item>
<!-- enum -->
<ion-list *ngIf="spec.type === 'enum'">
<ion-radio-group [(ngModel)]="value">
<ion-item *ngFor="let option of spec.values">
<ion-label>{{ spec['value-names'][option] }}</ion-label>
<ion-radio slot="start" [value]="option"></ion-radio>
</ion-item>
</ion-radio-group>
</ion-list>
<!-- metadata -->
<div class="ion-padding-start">
<p *ngIf="spec.type === 'string' && spec['pattern-description']">
{{ spec['pattern-description'] }}
</p>
<p *ngIf="spec.type === 'number' && spec.integral">
{{ integralDescription }}
</p>
<p *ngIf="rangeDescription">
{{ rangeDescription }}
</p>
<ng-container *ngIf="spec.default !== undefined">
<p>Default: {{ defaultDescription }} <ion-icon style="padding-left: 8px;" name="refresh-outline" color="dark" style="cursor: pointer;" (click)="refreshDefault()"></ion-icon></p>
<p *ngIf="spec.type === 'number' && spec.units">Units: {{ spec.units }}</p>
</ng-container>
</div>
</ion-item-group>
</ion-content>

View File

@@ -1,188 +0,0 @@
import { Component, Input } from '@angular/core'
import { getDefaultConfigValue, getDefaultDescription, Range } from 'src/app/pkg-config/config-utilities'
import { AlertController, LoadingController, ModalController, ToastController } from '@ionic/angular'
import { ConfigCursor } from 'src/app/pkg-config/config-cursor'
import { ValueSpecOf } from 'src/app/pkg-config/config-types'
import { copyToClipboard } from 'src/app/util/web.util'
import { ErrorToastService } from 'src/app/services/error-toast.service'
@Component({
selector: 'app-config-value',
templateUrl: 'app-config-value.page.html',
styleUrls: ['app-config-value.page.scss'],
})
export class AppConfigValuePage {
@Input() cursor: ConfigCursor<'string' | 'number' | 'boolean' | 'enum'>
@Input() saveFn?: (value: string | number | boolean) => Promise<any>
spec: ValueSpecOf<'string' | 'number' | 'boolean' | 'enum'>
value: string | number | boolean | null
edited: boolean
error: string
unmasked = false
defaultDescription: string
integralDescription = 'Value must be a whole number.'
range: Range
rangeDescription: string
constructor (
private readonly loadingCtrl: LoadingController,
private readonly modalCtrl: ModalController,
private readonly alertCtrl: AlertController,
private readonly toastCtrl: ToastController,
private readonly errToast: ErrorToastService,
) { }
ngOnInit () {
this.spec = this.cursor.spec()
this.value = this.cursor.config()
this.error = this.cursor.checkInvalid()
this.defaultDescription = getDefaultDescription(this.spec)
if (this.spec.type === 'number') {
this.range = Range.from(this.spec.range)
this.rangeDescription = this.range.description()
}
}
async dismiss () {
if (this.value === '') this.value = null
if (this.spec.type === 'number' && this.value !== null) {
this.value = Number(this.value)
}
if ((!!this.saveFn && this.edited) || (!this.saveFn && this.error)) {
await this.presentAlert()
} else {
await this.modalCtrl.dismiss(this.value)
}
}
async save () {
if (this.value === '') this.value = null
if (this.spec.type === 'number' && this.value !== null) {
this.value = Number(this.value)
}
const loader = await this.loadingCtrl.create({
spinner: 'lines',
message: 'Saving...',
cssClass: 'loader',
})
await loader.present()
try {
await this.saveFn(this.value)
this.modalCtrl.dismiss(this.value)
} catch (e) {
this.errToast.present(e)
} finally {
loader.dismiss()
}
}
refreshDefault () {
this.value = getDefaultConfigValue(this.spec) as any
this.handleInput()
}
handleInput () {
this.validate()
this.edited = true
}
clear () {
this.value = null
this.edited = true
}
toggleMask () {
this.unmasked = !this.unmasked
}
async copy (): Promise<void> {
let message = ''
await copyToClipboard(String(this.value)).then(success => { message = success ? 'copied to clipboard!' : 'failed to copy'})
const toast = await this.toastCtrl.create({
header: message,
position: 'bottom',
duration: 1000,
})
await toast.present()
}
private validate (): boolean {
if (this.spec.type === 'boolean') return true
// test blank
if (this.value === '' && !(this.spec as any).nullable) {
this.error = 'Value cannot be blank'
return false
}
// test pattern if string
if (this.spec.type === 'string' && this.value) {
const { pattern, 'pattern-description' : patternDescription } = this.spec
if (pattern && !RegExp(pattern).test(this.value as string)) {
this.error = patternDescription || `Must match ${pattern}`
return false
}
}
// test range if number
if (this.spec.type === 'number' && this.value) {
if (this.spec.integral && !RegExp(/^[-+]?[0-9]+$/).test(String(this.value))) {
this.error = this.integralDescription
return false
} else if (!this.spec.integral && !RegExp(/^[0-9]*\.?[0-9]+$/).test(String(this.value))) {
this.error = 'Value must be a number.'
return false
} else {
try {
this.range.checkIncludes(Number(this.value))
} catch (e) {
console.warn(e) //an invalid spec is not an error
this.error = e.message
return false
}
}
}
this.error = ''
return true
}
private async presentAlert () {
const header = this.error ?
'Invalid Entry' :
'Unsaved Changes'
const message = this.error ?
'Value will not be saved' :
'You have unsaved changes. Are you sure you want to leave?'
const alert = await this.alertCtrl.create({
backdropDismiss: false,
header,
message,
buttons: [
{
text: 'Cancel',
role: 'cancel',
},
{
text: `Leave`,
handler: () => {
this.modalCtrl.dismiss()
},
},
],
})
await alert.present()
}
}

View File

@@ -53,4 +53,8 @@ export class EnumListPage {
async toggleSelected (key: string) {
this.options[key] = !this.options[key]
}
asIsOrder () {
return 0
}
}