mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 20:14:49 +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
a43ff976a2
commit
5741cf084f
@@ -2,16 +2,17 @@ import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { AppActionInputPage } from './app-action-input.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'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { FormObjectComponentModule } from 'src/app/components/form-object/form-object.component.module'
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppActionInputPage],
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
ObjectConfigComponentModule,
|
||||
ConfigHeaderComponentModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
FormObjectComponentModule,
|
||||
],
|
||||
entryComponents: [AppActionInputPage],
|
||||
exports: [AppActionInputPage],
|
||||
|
||||
@@ -1,27 +1,29 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-button (click)="dismiss()" color="light">
|
||||
<ion-icon slot="icon-only" name="close-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
<ion-title>{{ action.name }}</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button [disabled]="error" (click)="save()">
|
||||
Save
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content>
|
||||
|
||||
<config-header [spec]="spec" [error]="error"></config-header>
|
||||
|
||||
<!-- object -->
|
||||
<ion-item-group>
|
||||
<ion-item-divider></ion-item-divider>
|
||||
<object-config [cursor]="cursor" (onEdit)="handleObjectEdit()"></object-config>
|
||||
</ion-item-group>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<form [formGroup]="actionForm" (ngSubmit)="save()" novalidate>
|
||||
<form-object
|
||||
[objectSpec]="action['input-spec']"
|
||||
[formGroup]="actionForm"
|
||||
></form-object>
|
||||
</form>
|
||||
</ion-content>
|
||||
|
||||
<ion-footer>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start" class="ion-padding-start">
|
||||
<ion-button fill="outline" (click)="dismiss()">
|
||||
Cancel
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
<ion-buttons slot="end" class="ion-padding-end">
|
||||
<ion-button fill="outline" color="primary" (click)="save()">
|
||||
Execute
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-footer>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
button:disabled,
|
||||
button[disabled]{
|
||||
border: 1px solid #999999;
|
||||
background-color: #cccccc;
|
||||
color: #666666;
|
||||
}
|
||||
button {
|
||||
color: var(--ion-color-primary);
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { LoadingController, ModalController } from '@ionic/angular'
|
||||
import { ConfigCursor } from 'src/app/pkg-config/config-cursor'
|
||||
import { ValueSpecObject } from 'src/app/pkg-config/config-types'
|
||||
import { ErrorToastService } from 'src/app/services/error-toast.service'
|
||||
import { FormGroup } from '@angular/forms'
|
||||
import { ModalController } from '@ionic/angular'
|
||||
import { Action } from 'src/app/services/patch-db/data-model'
|
||||
import { FormService } from 'src/app/services/form.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-action-input',
|
||||
@@ -12,22 +11,15 @@ import { Action } from 'src/app/services/patch-db/data-model'
|
||||
})
|
||||
export class AppActionInputPage {
|
||||
@Input() action: Action
|
||||
@Input() cursor: ConfigCursor<'object'>
|
||||
@Input() execute: () => Promise<void>
|
||||
spec: ValueSpecObject
|
||||
value: object
|
||||
error: string
|
||||
actionForm: FormGroup
|
||||
|
||||
constructor (
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly errToast: ErrorToastService,
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly formService: FormService,
|
||||
) { }
|
||||
|
||||
ngOnInit () {
|
||||
this.spec = this.cursor.spec()
|
||||
this.value = this.cursor.config()
|
||||
this.error = this.cursor.checkInvalid()
|
||||
this.actionForm = this.formService.createForm(this.action['input-spec'])
|
||||
}
|
||||
|
||||
async dismiss (): Promise<void> {
|
||||
@@ -35,24 +27,15 @@ export class AppActionInputPage {
|
||||
}
|
||||
|
||||
async save (): Promise<void> {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
spinner: 'lines',
|
||||
message: 'Executing action',
|
||||
cssClass: 'loader-ontop-of-all',
|
||||
})
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
await this.execute()
|
||||
this.modalCtrl.dismiss()
|
||||
} catch (e) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
if (this.actionForm.invalid) {
|
||||
this.actionForm.markAllAsTouched()
|
||||
document.getElementsByClassName('validation-error')[0].parentElement.parentElement.scrollIntoView({ behavior: 'smooth' })
|
||||
return
|
||||
}
|
||||
this.modalCtrl.dismiss(this.actionForm.value)
|
||||
}
|
||||
|
||||
handleObjectEdit (): void {
|
||||
this.error = this.cursor.checkInvalid()
|
||||
asIsOrder () {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,11 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-button (click)="dismiss()">
|
||||
<ion-icon name="arrow-back"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
<ion-title>{{ spec.name }}</ion-title>
|
||||
<ion-buttons *ngIf="spec.subtype !== 'enum'" slot="end">
|
||||
<ion-button (click)="presentModalValueEdit()">
|
||||
<ion-icon name="add" color="primary"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content>
|
||||
<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>
|
||||
@@ -46,7 +34,7 @@
|
||||
</ion-item-divider>
|
||||
|
||||
<div *ngFor="let v of value; index as i;">
|
||||
<ion-item button detail="false" (click)="presentModalValueEdit(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>
|
||||
@@ -55,7 +43,7 @@
|
||||
<ion-label>{{ valueString[i] }}</ion-label>
|
||||
|
||||
<ion-button slot="end" fill="clear" (click)="presentAlertDelete(i, $event)">
|
||||
<ion-icon slot="icon-only" name="close-outline"></ion-icon>
|
||||
<ion-icon slot="icon-only" name="close"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
</div>
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { AlertController } from '@ionic/angular'
|
||||
import { AlertController, IonNav } from '@ionic/angular'
|
||||
import { Annotations, Range } 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 { ValueSpecList, isValueSpecListOf } from 'src/app/pkg-config/config-types'
|
||||
import { ModalPresentable } from 'src/app/pkg-config/modal-presentable'
|
||||
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 extends ModalPresentable {
|
||||
export class AppConfigListPage {
|
||||
@Input() cursor: ConfigCursor<'list'>
|
||||
|
||||
spec: ValueSpecList
|
||||
@@ -22,7 +21,6 @@ export class AppConfigListPage extends ModalPresentable {
|
||||
// enum only
|
||||
options: { value: string, checked: boolean }[] = []
|
||||
selectAll = true
|
||||
//
|
||||
|
||||
min: number | undefined
|
||||
max: number | undefined
|
||||
@@ -34,10 +32,9 @@ export class AppConfigListPage extends ModalPresentable {
|
||||
|
||||
constructor (
|
||||
private readonly alertCtrl: AlertController,
|
||||
trackingModalCtrl: TrackingModalController,
|
||||
) {
|
||||
super(trackingModalCtrl)
|
||||
}
|
||||
private readonly subNav: SubNavService,
|
||||
private readonly nav: IonNav,
|
||||
) { }
|
||||
|
||||
ngOnInit () {
|
||||
this.spec = this.cursor.spec()
|
||||
@@ -59,10 +56,6 @@ export class AppConfigListPage extends ModalPresentable {
|
||||
this.updateCaches()
|
||||
}
|
||||
|
||||
async dismiss () {
|
||||
return this.dismissModal(this.value)
|
||||
}
|
||||
|
||||
// enum only
|
||||
toggleSelectAll () {
|
||||
if (!isValueSpecListOf(this.spec, 'enum')) { throw new Error('unreachable') }
|
||||
@@ -98,10 +91,10 @@ export class AppConfigListPage extends ModalPresentable {
|
||||
this.updateCaches()
|
||||
}
|
||||
|
||||
async presentModalValueEdit (index?: number) {
|
||||
async createOrEdit (index?: number) {
|
||||
const nextCursor = this.cursor.seekNext(index === undefined ? this.value.length : index)
|
||||
nextCursor.createFirstEntryForList()
|
||||
return this.presentModal(nextCursor, () => this.updateCaches())
|
||||
this.subNav.push(String(index), nextCursor, this.nav)
|
||||
}
|
||||
|
||||
async presentAlertDelete (key: number, e: Event) {
|
||||
|
||||
@@ -1,27 +1,4 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-button (click)="dismiss()">
|
||||
<ion-icon name="arrow-back"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
<ion-title>{{ spec.name }}</ion-title>
|
||||
<ion-buttons *ngIf="spec.nullable" slot="end">
|
||||
<ion-button color="danger" (click)="presentAlertDestroy()">
|
||||
Delete
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content>
|
||||
|
||||
<ion-content class="subheader-padding">
|
||||
<config-header [spec]="spec" [error]="error"></config-header>
|
||||
|
||||
<!-- object -->
|
||||
<ion-item-group>
|
||||
<ion-item-divider></ion-item-divider>
|
||||
<object-config [cursor]="cursor" (onEdit)="handleObjectEdit()"></object-config>
|
||||
</ion-item-group>
|
||||
|
||||
<object-config [cursor]="cursor" (onEdit)="handleObjectEdit()" (onClick)="updatePath($event)"></object-config>
|
||||
</ion-content>
|
||||
|
||||
@@ -1,15 +1,4 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-button (click)="dismiss()">
|
||||
<ion-icon name="arrow-back"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
<ion-title>{{ spec.name }}</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content>
|
||||
<ion-content class="subheader-padding">
|
||||
|
||||
<config-header [spec]="spec" [error]="error"></config-header>
|
||||
|
||||
@@ -24,10 +13,10 @@
|
||||
</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.variantNames[value[spec.tag.id]]"
|
||||
[(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.variantNames[option.key] }}
|
||||
{{ spec.tag['variant-names'][option.key] }}
|
||||
<span *ngIf="option.key === spec.default"> (default)</span>
|
||||
</ion-select-option>
|
||||
</ion-select>
|
||||
|
||||
@@ -46,8 +46,8 @@ export class AppConfigUnionPage {
|
||||
setSelectOptions () {
|
||||
return {
|
||||
header: this.spec.tag.name,
|
||||
subHeader: this.spec.changeWarning ? 'Warning!' : undefined,
|
||||
message: this.spec.changeWarning ? `${this.spec.changeWarning}` : undefined,
|
||||
subHeader: this.spec['change-warning'] ? 'Warning!' : undefined,
|
||||
message: this.spec['change-warning'] ? `${this.spec['change-warning']}` : undefined,
|
||||
cssClass: 'select-change-warning',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,4 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-button (click)="dismiss()">
|
||||
<ion-icon name="arrow-back"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
<ion-title>
|
||||
{{ spec.name }}
|
||||
</ion-title>
|
||||
<ion-buttons *ngIf="!!saveFn" slot="end">
|
||||
<ion-button [disabled]="!!error" (click)="save()">
|
||||
Save
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content>
|
||||
<ion-content class="subheader-padding">
|
||||
|
||||
<config-header [spec]="spec" [error]="error"></config-header>
|
||||
|
||||
@@ -55,15 +37,15 @@
|
||||
<ion-list *ngIf="spec.type === 'enum'">
|
||||
<ion-radio-group [(ngModel)]="value">
|
||||
<ion-item *ngFor="let option of spec.values">
|
||||
<ion-label>{{ spec.valueNames[option] }}</ion-label>
|
||||
<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.patternDescription">
|
||||
{{ spec.patternDescription }}
|
||||
<p *ngIf="spec.type === 'string' && spec['pattern-description']">
|
||||
{{ spec['pattern-description'] }}
|
||||
</p>
|
||||
<p *ngIf="spec.type === 'number' && spec.integral">
|
||||
{{ integralDescription }}
|
||||
|
||||
@@ -127,7 +127,7 @@ export class AppConfigValuePage {
|
||||
}
|
||||
// test pattern if string
|
||||
if (this.spec.type === 'string' && this.value) {
|
||||
const { pattern, patternDescription } = this.spec
|
||||
const { pattern, 'pattern-description' : patternDescription } = this.spec
|
||||
if (pattern && !RegExp(pattern).test(this.value as string)) {
|
||||
this.error = patternDescription || `Must match ${pattern}`
|
||||
return false
|
||||
|
||||
22
ui/src/app/modals/app-config/app-config.module.ts
Normal file
22
ui/src/app/modals/app-config/app-config.module.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { AppConfigPage } from './app-config.page'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
import { FormObjectComponentModule } from 'src/app/components/form-object/form-object.component.module'
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppConfigPage],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
IonicModule,
|
||||
SharingModule,
|
||||
FormObjectComponentModule,
|
||||
ReactiveFormsModule,
|
||||
],
|
||||
entryComponents: [AppConfigPage],
|
||||
exports: [AppConfigPage],
|
||||
})
|
||||
export class AppConfigPageModule { }
|
||||
94
ui/src/app/modals/app-config/app-config.page.html
Normal file
94
ui/src/app/modals/app-config/app-config.page.html
Normal file
@@ -0,0 +1,94 @@
|
||||
<ion-header>
|
||||
<ion-toolbar *ngIf="patch.data['package-data'][pkgId] as pkg">
|
||||
<ion-title>Config</ion-title>
|
||||
<ion-buttons slot="end" class="ion-padding-end">
|
||||
<ion-button fill="clear" [disabled]="loadingText" (click)="resetDefaults()">
|
||||
Reset Defaults
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
|
||||
<!-- loading -->
|
||||
<text-spinner *ngIf="loadingText; else loaded" [text]="loadingText"></text-spinner>
|
||||
|
||||
<!-- not loading -->
|
||||
<ng-template #loaded>
|
||||
|
||||
<ng-container *ngIf="patch.data['package-data'][pkgId] as pkg">
|
||||
<ng-container *ngIf="pkg.manifest.config && !pkg.installed.status.configured && !edited">
|
||||
<ion-item class="notifier-item">
|
||||
<ion-label class="ion-text-wrap">
|
||||
<h2 style="display: flex; align-items: center; margin-bottom: 3px;">
|
||||
<ion-icon size="small" style="margin-right: 5px" slot="start" color="dark" slot="start" name="alert-circle-outline"></ion-icon>
|
||||
<ion-text style="font-size: smaller;">Initial Config</ion-text>
|
||||
</h2>
|
||||
<p style="font-size: small">To use the default config for {{ pkg.manifest.title }}, click "Save" above.</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="rec && showRec">
|
||||
<ion-item class="rec-item">
|
||||
<ion-label class="ion-text-wrap">
|
||||
<h2 style="display: flex; align-items: center;">
|
||||
<ion-icon size="small" style="margin: 4px" slot="start" color="primary" slot="start" name="ellipse"></ion-icon>
|
||||
<ion-thumbnail style="width: 3vh; height: 3vh; margin: 0px 2px 0px 5px;" slot="start">
|
||||
<img [src]="rec.dependentIcon" [alt]="rec.dependentTitle"/>
|
||||
</ion-thumbnail>
|
||||
<ion-text style="margin: 5px; font-family: 'Montserrat'; font-size: smaller;">{{ rec.dependentTitle }}</ion-text>
|
||||
</h2>
|
||||
<div style="margin: 7px 5px;">
|
||||
<p style="font-size: small; color: var(--ion-color-medium)"> {{ pkg.manifest.title }} config has been modified to satisfy {{ rec.dependentTitle }}.
|
||||
<ion-text color="dark">To accept the changes, click “Save” above.</ion-text>
|
||||
</p>
|
||||
<a style="font-size: small" *ngIf="!openRec" (click)="openRec = true">More Info</a>
|
||||
<ng-container *ngIf="openRec">
|
||||
<p style="margin-top: 10px; color: var(--ion-color-medium); font-size: small" [innerHTML]="rec.description"></p>
|
||||
<a style="font-size: x-small; font-style: italic;" (click)="openRec = false">hide</a>
|
||||
</ng-container>
|
||||
<ion-button style="position: absolute; right: 0; top: 0" color="primary" fill="clear" (click)="dismissRec()">
|
||||
<ion-icon name="close"></ion-icon>
|
||||
</ion-button>
|
||||
</div>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item-divider></ion-item-divider>
|
||||
</ng-container>
|
||||
|
||||
<!-- no config -->
|
||||
<ion-item *ngIf="!hasConfig">
|
||||
<ion-label class="ion-text-wrap">
|
||||
<p>No config options for {{ pkg.manifest.title }} {{ pkg.manifest.version }}.</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
|
||||
<!-- has config -->
|
||||
<form [formGroup]="configForm" (ngSubmit)="save()" novalidate>
|
||||
<form-object
|
||||
[objectSpec]="configSpec"
|
||||
[formGroup]="configForm"
|
||||
[current]="current"
|
||||
showEdited
|
||||
></form-object>
|
||||
</form>
|
||||
</ng-template>
|
||||
</ion-content>
|
||||
|
||||
<ion-footer>
|
||||
<ion-toolbar *ngIf="patch.data['package-data'][pkgId] as pkg">
|
||||
<ion-buttons slot="start" class="ion-padding-start">
|
||||
<ion-button fill="outline" (click)="dismiss()">
|
||||
Cancel
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
<ion-buttons slot="end" class="ion-padding-end">
|
||||
<ion-button fill="outline" color="primary" [disabled]="loadingText" (click)="save(pkg)">
|
||||
Save
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-footer>
|
||||
8
ui/src/app/modals/app-config/app-config.page.scss
Normal file
8
ui/src/app/modals/app-config/app-config.page.scss
Normal file
@@ -0,0 +1,8 @@
|
||||
.notifier-item {
|
||||
margin: 12px;
|
||||
margin-top: 0px;
|
||||
border-radius: 12px;
|
||||
// kills the lines
|
||||
--border-width: 0;
|
||||
--inner-border-width: 0;
|
||||
}
|
||||
181
ui/src/app/modals/app-config/app-config.page.ts
Normal file
181
ui/src/app/modals/app-config/app-config.page.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
import { Component, Input, ViewChild } from '@angular/core'
|
||||
import { AlertController, ModalController, IonContent, LoadingController } from '@ionic/angular'
|
||||
import { ApiService } from 'src/app/services/api/embassy/embassy-api.service'
|
||||
import { isEmptyObject, Recommendation } from 'src/app/util/misc.util'
|
||||
import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component'
|
||||
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
|
||||
import { ConfigSpec } from 'src/app/pkg-config/config-types'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import { ErrorToastService } from 'src/app/services/error-toast.service'
|
||||
import { FormGroup } from '@angular/forms'
|
||||
import { FormService } from 'src/app/services/form.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-config',
|
||||
templateUrl: './app-config.page.html',
|
||||
styleUrls: ['./app-config.page.scss'],
|
||||
})
|
||||
export class AppConfigPage {
|
||||
@Input() pkgId: string
|
||||
loadingText: string | undefined
|
||||
configSpec: ConfigSpec
|
||||
configForm: FormGroup
|
||||
current: object
|
||||
hasConfig = false
|
||||
|
||||
rec: Recommendation | null = null
|
||||
showRec = true
|
||||
openRec = false
|
||||
|
||||
@ViewChild(IonContent) content: IonContent
|
||||
|
||||
constructor (
|
||||
private readonly wizardBaker: WizardBaker,
|
||||
private readonly embassyApi: ApiService,
|
||||
private readonly errToast: ErrorToastService,
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly formService: FormService,
|
||||
public readonly patch: PatchDbService,
|
||||
) { }
|
||||
|
||||
async ngOnInit () {
|
||||
const rec = history.state?.configRecommendation as Recommendation
|
||||
|
||||
try {
|
||||
this.loadingText = 'Loading Config'
|
||||
const { spec, config } = await this.embassyApi.getPackageConfig({ id: this.pkgId })
|
||||
|
||||
let depConfig: object
|
||||
if (rec) {
|
||||
this.loadingText = `Setting properties to accommodate ${rec.dependentTitle}...`
|
||||
depConfig = await this.embassyApi.dryConfigureDependency({ 'dependency-id': this.pkgId, 'dependent-id': rec.dependentId })
|
||||
}
|
||||
this.setConfig(spec, config, depConfig)
|
||||
} catch (e) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
this.loadingText = undefined
|
||||
}
|
||||
}
|
||||
|
||||
ngAfterViewInit () {
|
||||
this.content.scrollToPoint(undefined, 1)
|
||||
}
|
||||
|
||||
setConfig (spec: ConfigSpec, config: object, depConfig?: object) {
|
||||
this.configSpec = spec
|
||||
this.current = config
|
||||
this.hasConfig = !isEmptyObject(config)
|
||||
this.configForm = this.formService.createForm(spec, { ...config, ...depConfig })
|
||||
this.configForm.markAllAsTouched()
|
||||
|
||||
if (depConfig) {
|
||||
this.markDirtyRecursive(this.configForm, depConfig)
|
||||
}
|
||||
}
|
||||
|
||||
markDirtyRecursive (group: FormGroup, config: object) {
|
||||
Object.keys(config).forEach(key => {
|
||||
const next = group.get(key)
|
||||
if (!next) throw new Error('Dependency config not compatible with service version. Please contact support')
|
||||
const newVal = config[key]
|
||||
// check if val is an object
|
||||
if (newVal && typeof newVal === 'object' && !Array.isArray(newVal)) {
|
||||
this.markDirtyRecursive(next as FormGroup, newVal)
|
||||
} else {
|
||||
let val1 = group.get(key).value
|
||||
let val2 = config[key]
|
||||
if (Array.isArray(newVal)) {
|
||||
val1 = JSON.stringify(val1)
|
||||
val2 = JSON.stringify(val2)
|
||||
}
|
||||
if (val1 != val2) next.markAsDirty()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
resetDefaults () {
|
||||
this.configForm = this.formService.createForm(this.configSpec)
|
||||
this.markDirtyRecursive(this.configForm, this.current)
|
||||
}
|
||||
|
||||
dismissRec () {
|
||||
this.showRec = false
|
||||
}
|
||||
|
||||
async dismiss () {
|
||||
if (this.configForm.dirty) {
|
||||
await this.presentAlertUnsaved()
|
||||
} else {
|
||||
this.modalCtrl.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
async save (pkg: PackageDataEntry) {
|
||||
if (this.configForm.invalid) {
|
||||
document.getElementsByClassName('validation-error')[0].parentElement.parentElement.scrollIntoView({ behavior: 'smooth' })
|
||||
return
|
||||
}
|
||||
|
||||
const loader = await this.loadingCtrl.create({
|
||||
spinner: 'lines',
|
||||
message: `Saving config...`,
|
||||
cssClass: 'loader',
|
||||
})
|
||||
await loader.present()
|
||||
|
||||
const config = this.configForm.value
|
||||
|
||||
try {
|
||||
const breakages = await this.embassyApi.drySetPackageConfig({
|
||||
id: pkg.manifest.id,
|
||||
config,
|
||||
})
|
||||
|
||||
if (!isEmptyObject(breakages.length)) {
|
||||
const { cancelled } = await wizardModal(
|
||||
this.modalCtrl,
|
||||
this.wizardBaker.configure({
|
||||
pkg,
|
||||
breakages,
|
||||
}),
|
||||
)
|
||||
if (cancelled) return
|
||||
}
|
||||
|
||||
await this.embassyApi.setPackageConfig({
|
||||
id: pkg.manifest.id,
|
||||
config,
|
||||
})
|
||||
this.modalCtrl.dismiss()
|
||||
} catch (e) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private async presentAlertUnsaved () {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Unsaved Changes',
|
||||
message: 'You have unsaved changes. Are you sure you want to leave?',
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
role: 'cancel',
|
||||
},
|
||||
{
|
||||
text: `Leave`,
|
||||
handler: () => {
|
||||
this.modalCtrl.dismiss()
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
await alert.present()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding-top">
|
||||
|
||||
<text-spinner *ngIf="loading" text="Loading Drives"></text-spinner>
|
||||
|
||||
@@ -2,19 +2,15 @@ import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { AppRestoreComponent } from './app-restore.component'
|
||||
import { PwaBackComponentModule } from '../../components/pwa-back-button/pwa-back.component.module'
|
||||
import { BackupConfirmationComponentModule } from '../backup-confirmation/backup-confirmation.component.module'
|
||||
import { SharingModule } from '../../modules/sharing.module'
|
||||
import { TextSpinnerComponentModule } from '../../components/text-spinner/text-spinner.component.module'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
SharingModule,
|
||||
BackupConfirmationComponentModule,
|
||||
PwaBackComponentModule,
|
||||
TextSpinnerComponentModule,
|
||||
SharingModule,
|
||||
],
|
||||
declarations: [
|
||||
AppRestoreComponent,
|
||||
|
||||
@@ -31,7 +31,6 @@ export class AppRestoreComponent {
|
||||
) { }
|
||||
|
||||
ngOnInit () {
|
||||
console.log('initing')
|
||||
this.getExternalDisks()
|
||||
}
|
||||
|
||||
@@ -51,7 +50,6 @@ export class AppRestoreComponent {
|
||||
} catch (e) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
console.log('loading false')
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
@@ -80,7 +78,6 @@ export class AppRestoreComponent {
|
||||
}
|
||||
|
||||
private async restore (logicalname: string, password: string): Promise<void> {
|
||||
console.log('here here here')
|
||||
this.submitting = true
|
||||
// await loader.present()
|
||||
|
||||
|
||||
17
ui/src/app/modals/enum-list/enum-list.module.ts
Normal file
17
ui/src/app/modals/enum-list/enum-list.module.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { EnumListPage } from './enum-list.page'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
|
||||
@NgModule({
|
||||
declarations: [EnumListPage],
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
FormsModule,
|
||||
],
|
||||
entryComponents: [EnumListPage],
|
||||
exports: [EnumListPage],
|
||||
})
|
||||
export class EnumListPageModule { }
|
||||
36
ui/src/app/modals/enum-list/enum-list.page.html
Normal file
36
ui/src/app/modals/enum-list/enum-list.page.html
Normal file
@@ -0,0 +1,36 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>
|
||||
{{ spec.name }}
|
||||
</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button slot="end" fill="clear" color="primary" (click)="toggleSelectAll()">
|
||||
{{ selectAll ? 'All' : 'None' }}
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content>
|
||||
<ion-item-group>
|
||||
<ion-item *ngFor="let option of options | keyvalue : asIsOrder">
|
||||
<ion-label>{{ option.key }}</ion-label>
|
||||
<ion-checkbox slot="end" [(ngModel)]="option.value" (click)="toggleSelected(option.key)"></ion-checkbox>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
</ion-content>
|
||||
|
||||
<ion-footer>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start" class="ion-padding-start">
|
||||
<ion-button fill="outline" (click)="dismiss()">
|
||||
Cancel
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
<ion-buttons slot="end" class="ion-padding-end">
|
||||
<ion-button fill="outline" color="primary" (click)="save()">
|
||||
Done
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-footer>
|
||||
0
ui/src/app/modals/enum-list/enum-list.page.scss
Normal file
0
ui/src/app/modals/enum-list/enum-list.page.scss
Normal file
56
ui/src/app/modals/enum-list/enum-list.page.ts
Normal file
56
ui/src/app/modals/enum-list/enum-list.page.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { ModalController } from '@ionic/angular'
|
||||
import { ValueSpecListOf } from '../../pkg-config/config-types'
|
||||
// import { Range } from '../../pkg-config/config-utilities'
|
||||
|
||||
@Component({
|
||||
selector: 'enum-list',
|
||||
templateUrl: './enum-list.page.html',
|
||||
styleUrls: ['./enum-list.page.scss'],
|
||||
})
|
||||
export class EnumListPage {
|
||||
@Input() key: string
|
||||
@Input() spec: ValueSpecListOf<'enum'>
|
||||
@Input() current: string[]
|
||||
options: { [option: string]: boolean } = { }
|
||||
|
||||
// min: number | undefined
|
||||
// max: number | undefined
|
||||
// minMessage: string
|
||||
// maxMessage: string
|
||||
|
||||
selectAll = true
|
||||
|
||||
constructor (
|
||||
private readonly modalCtrl: ModalController,
|
||||
) { }
|
||||
|
||||
ngOnInit () {
|
||||
// const range = Range.from(this.spec.range)
|
||||
// this.min = range.integralMin()
|
||||
// this.max = range.integralMax()
|
||||
// this.minMessage = `The minimum number of ${this.key} is ${this.min}.`
|
||||
// this.maxMessage = `The maximum number of ${this.key} is ${this.max}.`
|
||||
|
||||
for (let val of this.spec.spec.values) {
|
||||
this.options[val] = this.current.includes(val)
|
||||
}
|
||||
}
|
||||
|
||||
dismiss () {
|
||||
this.modalCtrl.dismiss()
|
||||
}
|
||||
|
||||
save () {
|
||||
this.modalCtrl.dismiss(Object.keys(this.options).filter(key => this.options[key]))
|
||||
}
|
||||
|
||||
toggleSelectAll () {
|
||||
Object.keys(this.options).forEach(k => this.options[k] = this.selectAll)
|
||||
this.selectAll = !this.selectAll
|
||||
}
|
||||
|
||||
async toggleSelected (key: string) {
|
||||
this.options[key] = !this.options[key]
|
||||
}
|
||||
}
|
||||
@@ -3,14 +3,12 @@ import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { MarkdownPage } from './markdown.page'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
import { TextSpinnerComponentModule } from 'src/app/components/text-spinner/text-spinner.component.module'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
SharingModule,
|
||||
TextSpinnerComponentModule,
|
||||
],
|
||||
declarations: [MarkdownPage],
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-button (click)="dismiss()">
|
||||
<ion-icon slot="icon-only" name="close-outline"></ion-icon>
|
||||
<ion-icon slot="icon-only" name="close"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
<ion-title>{{ title | titlecase }}</ion-title>
|
||||
|
||||
Reference in New Issue
Block a user