ui: cleanup wizard

This commit is contained in:
Aaron Greenspan
2021-02-05 14:48:46 -07:00
committed by Aiden McClelland
parent 46643cb3a4
commit 52fc2c4011
10 changed files with 292 additions and 165 deletions

View File

@@ -3,7 +3,7 @@ import { BehaviorSubject, from, Subject } from 'rxjs'
import { takeUntil } from 'rxjs/operators'
import { markAsLoadingDuring$ } from 'src/app/services/loader.service'
import { capitalizeFirstLetter } from 'src/app/util/misc.util'
import { Colorable, Loadable } from '../loadable'
import { Loadable } from '../loadable'
import { WizardAction } from '../wizard-types'
@Component({
@@ -11,7 +11,7 @@ import { WizardAction } from '../wizard-types'
templateUrl: './complete.component.html',
styleUrls: ['../install-wizard.component.scss'],
})
export class CompleteComponent implements OnInit, Loadable, Colorable {
export class CompleteComponent implements OnInit, Loadable {
@Input() params: {
action: WizardAction
verb: string //loader verb: '*stopping* ...'
@@ -20,7 +20,13 @@ export class CompleteComponent implements OnInit, Loadable, Colorable {
skipCompletionDialogue?: boolean
}
@Input() finished: (info: { error?: Error, cancelled?: true, final?: true }) => Promise<any>
@Input() transitions: {
cancel: () => void
next: () => void
final: () => void
error: (e: Error) => void
}
$loading$ = new BehaviorSubject(false)
$color$ = new BehaviorSubject('medium')
@@ -32,8 +38,8 @@ export class CompleteComponent implements OnInit, Loadable, Colorable {
load () {
markAsLoadingDuring$(this.$loading$, from(this.params.executeAction())).pipe(takeUntil(this.$cancel$)).subscribe(
{ error: e => this.finished({ error: new Error(`${this.params.action} failed: ${e.message || e}`) }),
complete: () => this.params.skipCompletionDialogue && this.finished( { final: true} ),
{ error: e => this.transitions.error(new Error(`${this.params.action} failed: ${e.message || e}`)),
complete: () => this.params.skipCompletionDialogue && this.transitions.final(),
},
)
}

View File

@@ -5,7 +5,7 @@ import { AppStatus } from 'src/app/models/app-model'
import { AppDependency, DependencyViolationSeverity, getViolationSeverity } from 'src/app/models/app-types'
import { displayEmver } from 'src/app/pipes/emver.pipe'
import { InformationPopoverComponent } from '../../information-popover/information-popover.component'
import { Colorable, Loadable } from '../loadable'
import { Loadable } from '../loadable'
import { WizardAction } from '../wizard-types'
@Component({
@@ -13,7 +13,7 @@ import { WizardAction } from '../wizard-types'
templateUrl: './dependencies.component.html',
styleUrls: ['../install-wizard.component.scss'],
})
export class DependenciesComponent implements OnInit, Loadable, Colorable {
export class DependenciesComponent implements OnInit, Loadable {
@Input() params: {
action: WizardAction,
title: string,

View File

@@ -4,7 +4,7 @@ import { takeUntil, tap } from 'rxjs/operators'
import { DependentBreakage } from 'src/app/models/app-types'
import { markAsLoadingDuring$ } from 'src/app/services/loader.service'
import { capitalizeFirstLetter } from 'src/app/util/misc.util'
import { Colorable, Loadable } from '../loadable'
import { Loadable } from '../loadable'
import { WizardAction } from '../wizard-types'
@Component({
@@ -12,7 +12,7 @@ import { WizardAction } from '../wizard-types'
templateUrl: './dependents.component.html',
styleUrls: ['../install-wizard.component.scss'],
})
export class DependentsComponent implements OnInit, Loadable, Colorable {
export class DependentsComponent implements OnInit, Loadable {
@Input() params: {
title: string,
action: WizardAction, //Are you sure you want to *uninstall*...,
@@ -20,7 +20,12 @@ export class DependentsComponent implements OnInit, Loadable, Colorable {
fetchBreakages: () => Promise<DependentBreakage[]>,
skipConfirmationDialogue?: boolean
}
@Input() finished: (info: { error?: Error, cancelled?: true, final?: true }) => Promise<any>
@Input() transitions: {
cancel: () => void
next: () => void
final: () => void
error: (e: Error) => void
}
dependentBreakages: DependentBreakage[]
@@ -46,13 +51,13 @@ export class DependentsComponent implements OnInit, Loadable, Colorable {
this.longMessage = `${capitalizeFirstLetter(this.params.verb)} ${this.params.title} will cause the following services to STOP running. Starting them again will require additional actions.`
this.$color$.next('warning')
} else if (this.params.skipConfirmationDialogue) {
this.finished({ })
this.transitions.next()
} else {
this.longMessage = `No other services installed on your Embassy will be affected by this action.`
this.$color$.next('success')
}
},
error: (e: Error) => this.finished({ error: new Error(`Fetching dependent service information failed: ${e.message || e}`) }),
error: (e: Error) => this.transitions.error(new Error(`Fetching dependent service information failed: ${e.message || e}`)),
},
)
}

View File

@@ -9,11 +9,12 @@
<ion-content>
<ion-slides *ngIf="!($error$ | async)" id="slide-show" style="--bullet-background: white" pager="false">
<ion-slide *ngFor="let slide of params.slideDefinitions">
<dependencies #components *ngIf="slide.selector === 'dependencies'" [params]="slide.params"></dependencies>
<notes #components *ngIf="slide.selector === 'notes'" [params]="slide.params"></notes>
<dependents #components *ngIf="slide.selector === 'dependents'" [params]="slide.params" [finished]="finished"></dependents>
<complete #components *ngIf="slide.selector === 'complete'" [params]="slide.params" [finished]="finished"></complete>
<ion-slide *ngFor="let def of params.slideDefinitions">
<!-- We can pass [transitions]="transitions" into the component if logic within the component needs to trigger a transition (not just bottom bar) -->
<dependencies #components *ngIf="def.slide.selector === 'dependencies'" [params]="def.slide.params"></dependencies>
<notes #components *ngIf="def.slide.selector === 'notes'" [params]="def.slide.params"></notes>
<dependents #components *ngIf="def.slide.selector === 'dependents'" [params]="def.slide.params" [transitions]="transitions"></dependents>
<complete #components *ngIf="def.slide.selector === 'complete'" [params]="def.slide.params" [transitions]="transitions"></complete>
</ion-slide>
</ion-slides>
@@ -34,20 +35,31 @@
<ion-footer>
<ion-toolbar style="padding: 8px;">
<ng-container *ngIf="!($error$ | async)">
<ion-button slot="start" *ngIf="($anythingLoading$ | async) && currentSlideDef.cancelButton.whileLoading as cancel" (click)="finished({ cancelled: true })" class="toolbar-button" fill="outline" color="medium">
<ng-container *ngIf="!($initializing$ | async) && !($error$ | async) && { loading: currentSlide.$loading$ | async, bar: currentBottomBar} as v">
<!-- cancel button if loading/not loading -->
<ion-button slot="start" *ngIf="v.loading && v.bar.cancel.whileLoading as cancel" (click)="transitions.cancel()" class="toolbar-button" fill="outline" color="medium">
<ion-text *ngIf="cancel.text as t">{{t}}</ion-text>
<ion-icon *ngIf="!cancel.text" name="close-outline"></ion-icon>
</ion-button>
<ion-button slot="start" *ngIf="!($anythingLoading$ | async) && currentSlideDef.cancelButton.afterLoading as cancel" (click)="finished({ cancelled: true })" class="toolbar-button" fill="outline" color="medium">
<ion-button slot="start" *ngIf="!v.loading && v.bar.cancel.afterLoading as cancel" (click)="transitions.cancel()" class="toolbar-button" fill="outline" color="medium">
<ion-text *ngIf="cancel.text as t">{{t}}</ion-text>
<ion-icon *ngIf="!cancel.text" name="close-outline"></ion-icon>
</ion-button>
<ion-button slot="end" *ngIf="!($anythingLoading$ | async) && currentSlideDef.nextButton as nextButton" (click)="finished({})" fill="outline" class="toolbar-button" color="primary"><ion-text [class.smaller-text]="nextButton.length > 16">{{nextButton}}</ion-text></ion-button>
<ion-button slot="end" *ngIf="!($anythingLoading$ | async) && currentSlideDef.finishButton as finishButton" (click)="finished({ final: true })" fill="outline" class="toolbar-button" color="primary"><ion-text [class.smaller-text]="finishButton.length > 16">{{finishButton}}</ion-text></ion-button>
<!-- next button -->
<ion-button slot="end" *ngIf="!v.loading && v.bar.next as next" (click)="transitions.next()" fill="outline" class="toolbar-button" color="primary">
<ion-text [class.smaller-text]="next.length > 16">{{next}}</ion-text>
</ion-button>
<!-- finish button -->
<ion-button slot="end" *ngIf="!v.loading && v.bar.finish as finish" (click)="transitions.final()" fill="outline" class="toolbar-button" color="primary">
<ion-text [class.smaller-text]="finish.length > 16">{{finish}}</ion-text>
</ion-button>
</ng-container>
<ng-container *ngIf="$error$ | async">
<ion-button slot="start" (click)="finished({ final: true })" style="text-transform: capitalize; font-weight: bolder;" color="danger">Dismiss</ion-button>
<ion-button slot="start" (click)="transitions.final()" style="text-transform: capitalize; font-weight: bolder;" color="danger">Dismiss</ion-button>
</ng-container>
</ion-toolbar>
</ion-footer>

View File

@@ -1,15 +1,15 @@
import { Component, Input, NgZone, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core'
import { IonContent, IonSlides, ModalController } from '@ionic/angular'
import { BehaviorSubject, combineLatest, Subscription } from 'rxjs'
import { map } from 'rxjs/operators'
import { BehaviorSubject, from, Observable, of } from 'rxjs'
import { Cleanup } from 'src/app/util/cleanup'
import { capitalizeFirstLetter, pauseFor } from 'src/app/util/misc.util'
import { CompleteComponent } from './complete/complete.component'
import { DependenciesComponent } from './dependencies/dependencies.component'
import { DependentsComponent } from './dependents/dependents.component'
import { NotesComponent } from './notes/notes.component'
import { Colorable, Loadable } from './loadable'
import { Loadable } from './loadable'
import { WizardAction } from './wizard-types'
import { concatMap, switchMap } from 'rxjs/operators'
@Component({
selector: 'install-wizard',
@@ -18,36 +18,35 @@ import { WizardAction } from './wizard-types'
})
export class InstallWizardComponent extends Cleanup implements OnInit {
@Input() params: {
// defines the slideshow in the html
// defines each slide along with bottom bar
slideDefinitions: SlideDefinition[]
toolbar: TopbarParams
}
// containers
// content container so we can scroll to top between slide transitions
@ViewChild(IonContent) contentContainer: IonContent
// slide container gives us hook into transitioning slides
@ViewChild(IonSlides) slideContainer: IonSlides
//don't use this, use slideComponents instead.
//a slide component gives us hook into a slide. Allows us to call load when slide comes into view
@ViewChildren('components')
public slideComponentsQL: QueryList<Loadable & Colorable>
slideComponentsQL: QueryList<Loadable>
get slideComponents (): Loadable[] { return this.slideComponentsQL.toArray() }
//don't use this, use currentSlide instead.
slideIndex = 0
get slideComponents (): (Loadable & Colorable)[] {
return this.slideComponentsQL.toArray()
}
get currentSlide (): (Loadable & Colorable) {
private slideIndex = 0
get currentSlide (): Loadable {
return this.slideComponents[this.slideIndex]
}
get currentSlideDef (): SlideDefinition {
return this.params.slideDefinitions[this.slideIndex]
get currentSlideLoading$ (): Observable<boolean> {
return this.$initializing$.pipe(switchMap(
i => i ? of(true) : this.currentSlide.$loading$,
))
}
get currentBottomBar (): SlideDefinition['bottomBar'] {
return this.params.slideDefinitions[this.slideIndex].bottomBar
}
$anythingLoading$: BehaviorSubject<boolean> = new BehaviorSubject(true)
$currentColor$: BehaviorSubject<string> = new BehaviorSubject('medium')
$initializing$ = new BehaviorSubject(true)
$error$ = new BehaviorSubject(undefined)
constructor (private readonly modalController: ModalController, private readonly zone: NgZone) { super() }
@@ -60,26 +59,28 @@ export class InstallWizardComponent extends Cleanup implements OnInit {
}
ionViewDidEnter () {
this.cleanup(
combineLatest(this.slideComponents.map(component => component.$loading$)).pipe(
map(loadings => !loadings.every(p => !p)),
).subscribe(this.$anythingLoading$),
combineLatest(this.slideComponents.map(component => component.$color$)).pipe(
map(colors => colors[this.slideIndex]),
).subscribe(this.$currentColor$),
)
this.$initializing$.next(false)
}
finished = (info: { error?: Error, cancelled?: true, final?: true }) => {
// process bottom bar buttons
private transition = (info: { error?: Error, cancelled?: true, final?: true }) => {
if (info.cancelled) this.currentSlide.$cancel$.next()
if (info.final || info.cancelled) return this.modalController.dismiss(info)
if (info.error) return this.$error$.next(capitalizeFirstLetter(info.error.message))
this.slide()
this.moveToNextSlide()
}
private async slide () {
if (this.slideComponents[this.slideIndex + 1] === undefined) { return this.finished({ final: true }) }
// bottom bar button callbacks
transitions = {
cancel: () => this.transition({ cancelled: true }),
next: () => this.transition({ }),
final: () => this.transition({ final: true }),
error: e => this.transition({ error: e }),
}
private async moveToNextSlide () {
if (this.slideComponents[this.slideIndex + 1] === undefined) { return this.transition({ final: true }) }
this.zone.run(async () => {
this.slideComponents[this.slideIndex + 1].load()
await pauseFor(50)
@@ -92,33 +93,23 @@ export class InstallWizardComponent extends Cleanup implements OnInit {
}
}
export interface SlideCommon {
selector: string // component http selector
cancelButton: {
// indicates the existence of a cancel button, and whether to have text or an icon 'x' by default.
afterLoading?: { text?: string },
whileLoading?: { text?: string }
export interface SlideDefinition {
slide:
{ selector: 'dependencies', params: DependenciesComponent['params'] } |
{ selector: 'dependents', params: DependentsComponent['params'] } |
{ selector: 'complete', params: CompleteComponent['params'] } |
{ selector: 'notes', params: NotesComponent['params'] }
bottomBar: {
cancel: {
// indicates the existence of a cancel button, and whether to have text or an icon 'x' by default.
afterLoading?: { text?: string },
whileLoading?: { text?: string }
}
next?: string
finish?: string
}
nextButton?: string, // existence and content of next button
finishButton?: string // existence and content of finish button
}
export type SlideDefinition = SlideCommon & (
{
selector: 'dependencies',
params: DependenciesComponent['params']
} | {
selector: 'dependents',
params: DependentsComponent['params']
} | {
selector: 'complete',
params: CompleteComponent['params']
} | {
selector: 'notes',
params: NotesComponent['params']
}
)
export type TopbarParams = { action: WizardAction, title: string, version: string }
export async function wizardModal (

View File

@@ -1,11 +1,9 @@
import { BehaviorSubject, Subject } from 'rxjs'
export interface Loadable {
load: () => void
$loading$: BehaviorSubject<boolean> //will be true during load function
$cancel$: Subject<void> //will cancel load function
load: (prevResult?: any) => void
result?: any // fill this variable on slide 1 to get passed into the load on slide 2. If this variable is falsey, it will skip the next slide.
$loading$: BehaviorSubject<boolean> // will be true during load function
$cancel$: Subject<void> // will cancel load function
}
export interface Colorable {
$color$: BehaviorSubject<string>
}

View File

@@ -1,7 +1,7 @@
<div class="slide-content">
<div style="margin-top: 25px;">
<div style="margin: 15px; display: flex; justify-content: center; align-items: center;">
<ion-label [color]="$color$ | async" style="font-size: xx-large; font-weight: bold;">
<ion-label [color]="params.titleColor" style="font-size: xx-large; font-weight: bold;">
{{params.title}}
</ion-label>
</div>

View File

@@ -1,27 +1,24 @@
import { Component, Input, OnInit } from '@angular/core'
import { BehaviorSubject, Subject } from 'rxjs'
import { Colorable, Loadable } from '../loadable'
import { WizardAction } from '../wizard-types'
import { Loadable } from '../loadable'
@Component({
selector: 'notes',
templateUrl: './notes.component.html',
styleUrls: ['../install-wizard.component.scss'],
})
export class NotesComponent implements OnInit, Loadable, Colorable {
export class NotesComponent implements OnInit, Loadable {
@Input() params: {
action: WizardAction
notes: string
title: string
titleColor: string
}
$loading$ = new BehaviorSubject(false)
$color$ = new BehaviorSubject('light')
$cancel$ = new Subject<void>()
load () { }
constructor () { }
ngOnInit () { this.$color$.next(this.params.titleColor) }
ngOnInit () { }
}

View File

@@ -5,13 +5,14 @@ import { exists } from 'src/app/util/misc.util'
import { AppDependency, DependentBreakage, AppInstalledPreview } from '../../models/app-types'
import { ApiService } from '../../services/api/api.service'
import { InstallWizardComponent, SlideDefinition, TopbarParams } from './install-wizard.component'
import { WizardAction } from './wizard-types'
@Injectable({ providedIn: 'root' })
export class WizardBaker {
constructor (
private readonly apiService: ApiService,
private readonly updateService: OsUpdateService,
private readonly appModel: AppModel
private readonly appModel: AppModel,
) { }
install (values: {
@@ -28,17 +29,40 @@ export class WizardBaker {
const toolbar: TopbarParams = { action, title, version }
const slideDefinitions: SlideDefinition[] = [
installAlert ? { selector: 'notes', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Next', params: {
action, notes: installAlert, title: 'Warning', titleColor: 'warning',
}} : undefined,
{ selector: 'dependencies', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Install', params: {
action, title, version, serviceRequirements,
}},
{ selector: 'complete', finishButton: 'Dismiss', cancelButton: { whileLoading: { } }, params: {
action, verb: 'beginning installation for', title, executeAction: () => this.apiService.installApp(id, version).then(app => {
this.appModel.add({ ...app, status: AppStatus.INSTALLING })
}),
}},
installAlert ? {
slide: {
selector: 'notes',
params: { notes: installAlert, title: 'Warning', titleColor: 'warning' },
},
bottomBar: {
cancel: { afterLoading: { text: 'Cancel' } }, next: 'Next',
},
} : undefined,
{
slide: {
selector: 'dependencies',
params: { action, title, version, serviceRequirements },
},
bottomBar: {
cancel: { afterLoading: { text: 'Cancel' } },
next: 'Install',
},
},
{
slide: {
selector: 'complete',
params: {
action,
verb: 'beginning installation for',
title,
executeAction: () => this.apiService.installApp(id, version).then(app => { this.appModel.add({ ...app, status: AppStatus.INSTALLING })}),
},
},
bottomBar: {
cancel: { whileLoading: { } },
finish: 'Dismiss',
},
},
]
return { toolbar, slideDefinitions: slideDefinitions.filter(exists) }
}
@@ -57,20 +81,49 @@ export class WizardBaker {
const toolbar: TopbarParams = { action, title, version }
const slideDefinitions: SlideDefinition[] = [
installAlert ? { selector: 'notes', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Next', params: {
action, notes: installAlert, title: 'Warning', titleColor: 'warning',
}} : undefined,
{ selector: 'dependencies', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Update', params: {
action, title, version, serviceRequirements,
}},
{ selector: 'dependents', cancelButton: { whileLoading: { }, afterLoading: { text: 'Cancel' } }, nextButton: 'Update Anyways', params: {
skipConfirmationDialogue: true, action, verb: 'updating', title, fetchBreakages: () => this.apiService.installApp(id, version, true).then( ({ breakages }) => breakages ),
}},
{ selector: 'complete', finishButton: 'Dismiss', cancelButton: { whileLoading: { } }, params: {
action, verb: 'beginning update for', title, executeAction: () => this.apiService.installApp(id, version).then(app => {
this.appModel.update({ id: app.id, status: AppStatus.INSTALLING })
}),
}},
installAlert ? {
slide: {
selector: 'notes',
params: { notes: installAlert, title: 'Warning', titleColor: 'warning'},
},
bottomBar: {
cancel: { afterLoading: { text: 'Cancel' } },
next: 'Next',
},
} : undefined,
{ slide: {
selector: 'dependencies',
params: { action, title, version, serviceRequirements },
},
bottomBar: {
cancel: { afterLoading: { text: 'Cancel' } },
next: 'Update',
},
},
{ slide: {
selector: 'dependents',
params: {
skipConfirmationDialogue: true, action, verb: 'updating', title, fetchBreakages: () => this.apiService.installApp(id, version, true).then( ({ breakages }) => breakages ),
},
},
bottomBar: {
cancel: { afterLoading: { text: 'Cancel' } },
next: 'Update Anyways',
},
},
{ slide: {
selector: 'complete',
params: {
action, verb: 'beginning update for', title, executeAction: () => this.apiService.installApp(id, version).then(app => {
this.appModel.update({ id: app.id, status: AppStatus.INSTALLING })
}),
},
},
bottomBar: {
cancel: { whileLoading: { } },
finish: 'Dismiss',
},
},
]
return { toolbar, slideDefinitions: slideDefinitions.filter(exists) }
}
@@ -85,12 +138,25 @@ export class WizardBaker {
const toolbar: TopbarParams = { action, title, version }
const slideDefinitions: SlideDefinition[] = [
{ selector: 'notes', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Update OS', params: {
action, notes: releaseNotes || 'No release notes for this version', title: 'Release Notes', titleColor: 'dark',
}},
{ selector: 'complete', finishButton: 'Dismiss', cancelButton: { whileLoading: { } }, params: {
action, verb: 'beginning update for', title, executeAction: () => this.updateService.updateEmbassyOS(version),
}},
{ slide : {
selector: 'notes',
params: { notes: releaseNotes, title: 'Release Notes', titleColor: 'dark' },
},
bottomBar: {
cancel: { afterLoading: { text: 'Cancel' } }, next: 'Update OS',
},
},
{ slide: {
selector: 'complete',
params: {
action, verb: 'beginning update for', title, executeAction: () => this.updateService.updateEmbassyOS(version),
},
},
bottomBar: {
cancel: { whileLoading: { }},
finish: 'Dismiss',
},
},
]
return { toolbar, slideDefinitions: slideDefinitions.filter(exists) }
}
@@ -109,20 +175,45 @@ export class WizardBaker {
const toolbar: TopbarParams = { action, title, version }
const slideDefinitions: SlideDefinition[] = [
installAlert ? { selector: 'notes', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Next', params: {
action, notes: installAlert, title: 'Warning', titleColor: 'warning',
}} : undefined,
{ selector: 'dependencies', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Downgrade', params: {
action, title, version, serviceRequirements,
}},
{ selector: 'dependents', cancelButton: { whileLoading: { }, afterLoading: { text: 'Cancel' } }, nextButton: 'Downgrade Anyways', params: {
skipConfirmationDialogue: true, action, verb: 'downgrading', title, fetchBreakages: () => this.apiService.installApp(id, version, true).then( ({ breakages }) => breakages ),
}},
{ selector: 'complete', finishButton: 'Dismiss', cancelButton: { whileLoading: { } }, params: {
action, verb: 'beginning downgrade for', title, executeAction: () => this.apiService.installApp(id, version).then(app => {
this.appModel.update({ id: app.id, status: AppStatus.INSTALLING })
}),
}},
installAlert ? {
slide: {
selector: 'notes',
params: { notes: installAlert, title: 'Warning', titleColor: 'warning' },
},
bottomBar: { cancel: { afterLoading: { text: 'Cancel' } }, next: 'Next' },
} : undefined,
{ slide: {
selector: 'dependencies',
params: { action, title, version, serviceRequirements },
},
bottomBar: {
cancel: { afterLoading: { text: 'Cancel' } },
next: 'Downgrade',
},
},
{ slide: {
selector: 'dependents',
params: {
skipConfirmationDialogue: true, action, verb: 'downgrading', title, fetchBreakages: () => this.apiService.installApp(id, version, true).then( ({ breakages }) => breakages ),
},
},
bottomBar: {
cancel: { whileLoading: { }, afterLoading: { text: 'Cancel' } }, next: 'Downgrade Anyways',
},
},
{ slide: {
selector: 'complete',
params: {
action, verb: 'beginning downgrade for', title, executeAction: () => this.apiService.installApp(id, version).then(app => {
this.appModel.update({ id: app.id, status: AppStatus.INSTALLING })
}),
},
},
bottomBar: {
cancel: { whileLoading: { } },
finish: 'Dismiss',
},
},
]
return { toolbar, slideDefinitions: slideDefinitions.filter(exists) }
}
@@ -136,19 +227,36 @@ export class WizardBaker {
validate(title, exists, 'missing title')
validate(version, exists, 'missing version')
const action = 'uninstall'
const action = 'uninstall' as WizardAction
const toolbar: TopbarParams = { action, title, version }
const slideDefinitions: SlideDefinition[] = [
{ selector: 'notes', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Continue', params: {
action, notes: uninstallAlert || defaultUninstallationWarning(title), title: 'Warning', titleColor: 'warning' },
{ slide: {
selector: 'notes',
params: {
notes: uninstallAlert || defaultUninstallationWarning(title),
title: 'Warning',
titleColor: 'warning',
},
},
bottomBar: { cancel: { afterLoading: { text: 'Cancel' } }, next: 'Continue' },
},
{ slide: {
selector: 'dependents',
params: {
action, verb: 'uninstalling', title, fetchBreakages: () => this.apiService.uninstallApp(id, true).then( ({ breakages }) => breakages ),
},
},
bottomBar: { cancel: { whileLoading: { }, afterLoading: { text: 'Cancel' } }, next: 'Uninstall' },
},
{ slide: {
selector: 'complete',
params: {
action, verb: 'uninstalling', title, executeAction: () => this.apiService.uninstallApp(id).then(() => this.appModel.delete(id)),
},
},
bottomBar: { finish: 'Dismiss', cancel: { whileLoading: { } } },
},
{ selector: 'dependents', cancelButton: { whileLoading: { }, afterLoading: { text: 'Cancel' } }, nextButton: 'Uninstall', params: {
action, verb: 'uninstalling', title, fetchBreakages: () => this.apiService.uninstallApp(id, true).then( ({ breakages }) => breakages ),
}},
{ selector: 'complete', finishButton: 'Dismiss', cancelButton: { whileLoading: { } }, params: {
action, verb: 'uninstalling', title, executeAction: () => this.apiService.uninstallApp(id).then(() => this.appModel.delete(id)),
}},
]
return { toolbar, slideDefinitions: slideDefinitions.filter(exists) }
}
@@ -166,9 +274,14 @@ export class WizardBaker {
const toolbar: TopbarParams = { action, title, version }
const slideDefinitions: SlideDefinition[] = [
{ selector: 'dependents', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Stop Anyways', params: {
action, verb: 'stopping', title, fetchBreakages: () => Promise.resolve(breakages),
}},
{ slide: {
selector: 'dependents',
params: {
action, verb: 'stopping', title, fetchBreakages: () => Promise.resolve(breakages),
},
},
bottomBar: { cancel: { afterLoading: { text: 'Cancel' } }, next: 'Stop Anyways' },
},
]
return { toolbar, slideDefinitions }
}
@@ -182,9 +295,14 @@ export class WizardBaker {
const toolbar: TopbarParams = { action, title, version }
const slideDefinitions: SlideDefinition[] = [
{ selector: 'dependents', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Save Config Anyways', params: {
action, verb: 'saving config for', title, fetchBreakages: () => Promise.resolve(breakages),
}},
{ slide: {
selector: 'dependents',
params: {
action, verb: 'saving config for', title, fetchBreakages: () => Promise.resolve(breakages),
},
},
bottomBar: { cancel: { afterLoading: { text: 'Cancel' } }, next: 'Save Config Anyways' },
},
]
return { toolbar, slideDefinitions }
}

View File

@@ -15,6 +15,7 @@ import { Cleanup } from 'src/app/util/cleanup'
import { InformationPopoverComponent } from 'src/app/components/information-popover/information-popover.component'
import { Emver } from 'src/app/services/emver.service'
import { displayEmver } from 'src/app/pipes/emver.pipe'
import { pauseFor } from 'src/app/util/misc.util'
@Component({
selector: 'app-available-show',
@@ -160,6 +161,7 @@ export class AppAvailableShowPage extends Cleanup {
}),
)
if (cancelled) return
await pauseFor(250)
this.navCtrl.back()
}
@@ -174,18 +176,16 @@ export class AppAvailableShowPage extends Cleanup {
installAlert: app.installAlert,
}
switch (action) {
case 'update':
return wizardModal(
this.modalCtrl,
this.wizardBaker.update(value),
).then(({ cancelled }) => cancelled || this.navCtrl.back())
case 'downgrade':
return wizardModal(
this.modalCtrl,
this.wizardBaker.downgrade(value),
).then(({ cancelled }) => cancelled || this.navCtrl.back())
}
const { cancelled } = await wizardModal(
this.modalCtrl,
action === 'update' ?
this.wizardBaker.update(value) :
this.wizardBaker.downgrade(value),
)
if (cancelled) return
await pauseFor(250)
this.navCtrl.back()
}
private fetchRecommendation (): Observable<any> {