mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-04-01 21:13:09 +00:00
feat: move all frontend projects under the same Angular workspace (#1141)
* feat: move all frontend projects under the same Angular workspace * Refactor/angular workspace (#1154) * update frontend build steps Co-authored-by: waterplea <alexander@inkin.ru> Co-authored-by: Matt Hill <matthewonthemoon@gmail.com> Co-authored-by: Lucy Cifferello <12953208+elvece@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { ActionSuccessPage } from './action-success.page'
|
||||
import { QrCodeModule } from 'ng-qrcode'
|
||||
|
||||
@NgModule({
|
||||
declarations: [ActionSuccessPage],
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
QrCodeModule,
|
||||
],
|
||||
exports: [ActionSuccessPage],
|
||||
})
|
||||
export class ActionSuccessPageModule { }
|
||||
@@ -0,0 +1,30 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-button (click)="dismiss()" class="enter-click">
|
||||
<ion-icon name="close"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Execution Complete</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content>
|
||||
<ion-item>
|
||||
<ion-label>
|
||||
<h2>{{ actionRes.message }}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<div *ngIf="actionRes.value" class="ion-text-center" style="padding: 64px 0;">
|
||||
<div *ngIf="actionRes.qr" class="ion-padding-bottom">
|
||||
<qr-code [value]="actionRes.value" size="240"></qr-code>
|
||||
</div>
|
||||
|
||||
<p *ngIf="!actionRes.copyable">{{ actionRes.value }}</p>
|
||||
<a *ngIf="actionRes.copyable" style="cursor: copy;" (click)="copy(actionRes.value)">
|
||||
<b>{{ actionRes.value }}</b>
|
||||
<sup><ion-icon name="copy-outline" style="padding-left: 6px; font-size: small;"></ion-icon></sup>
|
||||
</a>
|
||||
</div>
|
||||
</ion-content>
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { ModalController, ToastController } from '@ionic/angular'
|
||||
import { ActionResponse } from 'src/app/services/api/api.types'
|
||||
import { copyToClipboard } from 'src/app/util/web.util'
|
||||
|
||||
@Component({
|
||||
selector: 'action-success',
|
||||
templateUrl: './action-success.page.html',
|
||||
styleUrls: ['./action-success.page.scss'],
|
||||
})
|
||||
export class ActionSuccessPage {
|
||||
@Input() actionRes: ActionResponse
|
||||
|
||||
constructor (
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly toastCtrl: ToastController,
|
||||
) { }
|
||||
|
||||
async copy (address: string) {
|
||||
let message = ''
|
||||
await copyToClipboard(address || '')
|
||||
.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()
|
||||
}
|
||||
|
||||
async dismiss () {
|
||||
return this.modalCtrl.dismiss()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
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,
|
||||
],
|
||||
exports: [AppConfigPage],
|
||||
})
|
||||
export class AppConfigPageModule { }
|
||||
@@ -0,0 +1,91 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-button (click)="dismiss()">
|
||||
<ion-icon slot="icon-only" name="close"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Config</ion-title>
|
||||
<ion-buttons *ngIf="!loadingText && !loadingError && hasConfig" slot="end" class="ion-padding-end">
|
||||
<ion-button fill="clear" (click)="resetDefaults()">
|
||||
<ion-icon slot="start" name="refresh"></ion-icon>
|
||||
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>
|
||||
|
||||
<ion-item *ngIf="loadingError; else noError">
|
||||
<ion-label>
|
||||
<ion-text color="danger">
|
||||
{{ loadingError }}
|
||||
</ion-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ng-template #noError>
|
||||
<ion-item *ngIf="hasConfig && !pkg.installed.status.configured && !configForm.dirty">
|
||||
<ion-label>
|
||||
<ion-text color="success">To use the default config for {{ pkg.manifest.title }}, click "Save" below.</ion-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<!-- auto-config -->
|
||||
<ion-item lines="none" *ngIf="dependentInfo" class="rec-item" style="margin-bottom: 48px;">
|
||||
<ion-label>
|
||||
<h2 style="display: flex; align-items: center;">
|
||||
<img style="width: 18px; margin: 4px;" [src]="pkg['static-files'].icon" [alt]="pkg.manifest.title"/>
|
||||
<ion-text style="margin: 5px; font-family: 'Montserrat'; font-size: 18px;">{{ pkg.manifest.title }}</ion-text>
|
||||
</h2>
|
||||
<p>
|
||||
<ion-text color="dark">
|
||||
The following modifications have been made to {{ pkg.manifest.title }} to satisfy {{ dependentInfo.title }}:
|
||||
<ul>
|
||||
<li *ngFor="let d of diff" [innerHtml]="d"></li>
|
||||
</ul>
|
||||
To accept these modifications, click "Save".
|
||||
</ion-text>
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<!-- no config -->
|
||||
<ion-item *ngIf="!hasConfig">
|
||||
<ion-label>
|
||||
<p>No config options for {{ pkg.manifest.title }} {{ pkg.manifest.version }}.</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<!-- has config -->
|
||||
<form *ngIf="hasConfig" [formGroup]="configForm" novalidate>
|
||||
<form-object
|
||||
[objectSpec]="configSpec"
|
||||
[formGroup]="configForm"
|
||||
[current]="configForm.value"
|
||||
[showEdited]="true"
|
||||
></form-object>
|
||||
</form>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</ion-content>
|
||||
|
||||
<ion-footer>
|
||||
<ion-toolbar>
|
||||
<ion-buttons *ngIf="!loadingText && !loadingError" slot="end" class="ion-padding-end">
|
||||
<ion-button *ngIf="hasConfig" fill="outline" [disabled]="saving" (click)="save()" class="enter-click" [class.no-click]="saving">
|
||||
Save
|
||||
</ion-button>
|
||||
<ion-button *ngIf="!hasConfig" fill="outline" (click)="dismiss()" class="enter-click">
|
||||
Close
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-footer>
|
||||
@@ -0,0 +1,8 @@
|
||||
.notifier-item {
|
||||
margin: 12px;
|
||||
margin-top: 0px;
|
||||
border-radius: 12px;
|
||||
// kills the lines
|
||||
--border-width: 0;
|
||||
--inner-border-width: 0;
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
import { Component, Input, ViewChild } from '@angular/core'
|
||||
import { AlertController, ModalController, IonContent, LoadingController, IonicSafeString } from '@ionic/angular'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { DependentInfo, isEmptyObject, isObject } 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, getErrorMessage } from 'src/app/services/error-toast.service'
|
||||
import { FormGroup } from '@angular/forms'
|
||||
import { convertValuesRecursive, FormService } from 'src/app/services/form.service'
|
||||
import { compare, Operation, getValueByPointer } from 'fast-json-patch'
|
||||
|
||||
@Component({
|
||||
selector: 'app-config',
|
||||
templateUrl: './app-config.page.html',
|
||||
styleUrls: ['./app-config.page.scss'],
|
||||
})
|
||||
export class AppConfigPage {
|
||||
@ViewChild(IonContent) content: IonContent
|
||||
@Input() pkgId: string
|
||||
@Input() dependentInfo?: DependentInfo
|
||||
diff: string[] // only if dependent info
|
||||
pkg: PackageDataEntry
|
||||
loadingText: string | undefined
|
||||
configSpec: ConfigSpec
|
||||
configForm: FormGroup
|
||||
original: object
|
||||
hasConfig = false
|
||||
saving = false
|
||||
loadingError: string | IonicSafeString
|
||||
|
||||
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,
|
||||
private readonly patch: PatchDbService,
|
||||
) { }
|
||||
|
||||
async ngOnInit () {
|
||||
this.pkg = this.patch.getData()['package-data'][this.pkgId]
|
||||
this.hasConfig = !!this.pkg.manifest.config
|
||||
|
||||
if (!this.hasConfig) return
|
||||
|
||||
try {
|
||||
let oldConfig: object
|
||||
let newConfig: object
|
||||
let spec: ConfigSpec
|
||||
let patch: Operation[]
|
||||
if (this.dependentInfo) {
|
||||
this.loadingText = `Setting properties to accommodate ${this.dependentInfo.title}`
|
||||
const { 'old-config': oc, 'new-config': nc, spec: s } = await this.embassyApi.dryConfigureDependency({ 'dependency-id': this.pkgId, 'dependent-id': this.dependentInfo.id })
|
||||
oldConfig = oc
|
||||
newConfig = nc
|
||||
spec = s
|
||||
patch = compare(oldConfig, newConfig)
|
||||
} else {
|
||||
this.loadingText = 'Loading Config'
|
||||
const { config: c, spec: s } = await this.embassyApi.getPackageConfig({ id: this.pkgId })
|
||||
oldConfig = c
|
||||
spec = s
|
||||
}
|
||||
|
||||
this.original = oldConfig
|
||||
this.configSpec = spec
|
||||
this.configForm = this.formService.createForm(spec, newConfig || oldConfig)
|
||||
this.configForm.markAllAsTouched()
|
||||
|
||||
if (patch) {
|
||||
this.diff = this.getDiff(patch)
|
||||
this.markDirty(patch)
|
||||
}
|
||||
} catch (e) {
|
||||
this.loadingError = getErrorMessage(e)
|
||||
} finally {
|
||||
this.loadingText = undefined
|
||||
}
|
||||
}
|
||||
|
||||
ngAfterViewInit () {
|
||||
this.content.scrollToPoint(undefined, 1)
|
||||
}
|
||||
|
||||
resetDefaults () {
|
||||
this.configForm = this.formService.createForm(this.configSpec)
|
||||
const patch = compare(this.original, this.configForm.value)
|
||||
this.markDirty(patch)
|
||||
}
|
||||
|
||||
async dismiss () {
|
||||
if (this.configForm?.dirty) {
|
||||
await this.presentAlertUnsaved()
|
||||
} else {
|
||||
this.modalCtrl.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
async save () {
|
||||
convertValuesRecursive(this.configSpec, this.configForm)
|
||||
|
||||
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()
|
||||
|
||||
this.saving = true
|
||||
|
||||
try {
|
||||
const config = this.configForm.value
|
||||
|
||||
const breakages = await this.embassyApi.drySetPackageConfig({
|
||||
id: this.pkgId,
|
||||
config,
|
||||
})
|
||||
|
||||
if (!isEmptyObject(breakages['length'])) {
|
||||
const { cancelled } = await wizardModal(
|
||||
this.modalCtrl,
|
||||
this.wizardBaker.configure({
|
||||
pkg: this.pkg,
|
||||
breakages,
|
||||
}),
|
||||
)
|
||||
if (cancelled) return
|
||||
}
|
||||
|
||||
await this.embassyApi.setPackageConfig({
|
||||
id: this.pkgId,
|
||||
config,
|
||||
})
|
||||
this.modalCtrl.dismiss()
|
||||
} catch (e) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
this.saving = false
|
||||
loader.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private getDiff (patch: Operation[]): string[] {
|
||||
return patch.map(op => {
|
||||
let message: string
|
||||
switch (op.op) {
|
||||
case 'add':
|
||||
message = `Added ${this.getNewValue(op.value)}`
|
||||
break
|
||||
case 'remove':
|
||||
message = `Removed ${this.getOldValue(op.path)}`
|
||||
break
|
||||
case 'replace':
|
||||
message = `Changed from ${this.getOldValue(op.path)} to ${this.getNewValue(op.value)}`
|
||||
break
|
||||
default:
|
||||
message = `Unknown operation`
|
||||
}
|
||||
|
||||
let displayPath: string
|
||||
|
||||
const arrPath = op.path.substring(1)
|
||||
.split('/')
|
||||
.map(node => {
|
||||
const num = Number(node)
|
||||
return isNaN(num) ? node : num
|
||||
})
|
||||
|
||||
if (typeof arrPath[arrPath.length - 1] === 'number') {
|
||||
arrPath.pop()
|
||||
}
|
||||
|
||||
displayPath = arrPath.join(' → ')
|
||||
|
||||
return `${displayPath}: ${message}`
|
||||
})
|
||||
}
|
||||
|
||||
private getOldValue (path: any): string {
|
||||
const val = getValueByPointer(this.original, path)
|
||||
if (['string', 'number', 'boolean'].includes(typeof val)) {
|
||||
return val
|
||||
} else if (isObject(val)) {
|
||||
return 'entry'
|
||||
} else {
|
||||
return 'list'
|
||||
}
|
||||
}
|
||||
|
||||
private getNewValue (val: any): string {
|
||||
if (['string', 'number', 'boolean'].includes(typeof val)) {
|
||||
return val
|
||||
} else if (isObject(val)) {
|
||||
return 'new entry'
|
||||
} else {
|
||||
return 'new list'
|
||||
}
|
||||
}
|
||||
|
||||
private markDirty (patch: Operation[]) {
|
||||
patch.forEach(op => {
|
||||
const arrPath = op.path.substring(1)
|
||||
.split('/')
|
||||
.map(node => {
|
||||
const num = Number(node)
|
||||
return isNaN(num) ? node : num
|
||||
})
|
||||
|
||||
if (op.op !== 'remove') this.configForm.get(arrPath).markAsDirty()
|
||||
|
||||
if (typeof arrPath[arrPath.length - 1] === 'number') {
|
||||
const prevPath = arrPath.slice(0, arrPath.length - 1)
|
||||
this.configForm.get(prevPath).markAsDirty()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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()
|
||||
},
|
||||
cssClass: 'enter-click',
|
||||
},
|
||||
],
|
||||
})
|
||||
await alert.present()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { AppRecoverSelectPage } from './app-recover-select.page'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppRecoverSelectPage],
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
FormsModule,
|
||||
],
|
||||
exports: [AppRecoverSelectPage],
|
||||
})
|
||||
export class AppRecoverSelectPageModule { }
|
||||
@@ -0,0 +1,42 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Select Services to Restore</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button (click)="dismiss()">
|
||||
<ion-icon slot="icon-only" name="close"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content>
|
||||
<ion-item-group>
|
||||
<ion-item *ngFor="let option of options">
|
||||
<ion-label>
|
||||
<h2>{{ option.title }}</h2>
|
||||
<p>Version {{ option.version }}</p>
|
||||
<p>Backup made: {{ option.timestamp | date : 'short' }}</p>
|
||||
<p *ngIf="!option.installed && !option['newer-eos']">
|
||||
<ion-text color="success">Ready to restore</ion-text>
|
||||
</p>
|
||||
<p *ngIf="option.installed">
|
||||
<ion-text color="warning">Unavailable. {{ option.title }} is already installed.</ion-text>
|
||||
</p>
|
||||
<p *ngIf="option['newer-eos']">
|
||||
<ion-text color="danger">Unavailable. Backup was made on a newer version of EmbassyOS.</ion-text>
|
||||
</p>
|
||||
</ion-label>
|
||||
<ion-checkbox slot="end" [(ngModel)]="option.checked" [disabled]="option.installed || option['newer-eos']" (ionChange)="handleChange()"></ion-checkbox>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
</ion-content>
|
||||
|
||||
<ion-footer>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="end" class="ion-padding-end">
|
||||
<ion-button [disabled]="!hasSelection" fill="outline" (click)="restore()" class="enter-click">
|
||||
Restore Selected
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-footer>
|
||||
@@ -0,0 +1,84 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { LoadingController, ModalController, IonicSafeString } from '@ionic/angular'
|
||||
import { BackupInfo, PackageBackupInfo } from 'src/app/services/api/api.types'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { Emver } from 'src/app/services/emver.service'
|
||||
import { getErrorMessage } from 'src/app/services/error-toast.service'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-recover-select',
|
||||
templateUrl: './app-recover-select.page.html',
|
||||
styleUrls: ['./app-recover-select.page.scss'],
|
||||
})
|
||||
export class AppRecoverSelectPage {
|
||||
@Input() id: string
|
||||
@Input() backupInfo: BackupInfo
|
||||
@Input() password: string
|
||||
@Input() oldPassword: string
|
||||
options: (PackageBackupInfo & {
|
||||
id: string
|
||||
checked: boolean
|
||||
installed: boolean
|
||||
'newer-eos': boolean
|
||||
})[]
|
||||
hasSelection = false
|
||||
error: string | IonicSafeString
|
||||
|
||||
constructor (
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly embassyApi: ApiService,
|
||||
private readonly config: ConfigService,
|
||||
private readonly emver: Emver,
|
||||
private readonly patch: PatchDbService,
|
||||
) { }
|
||||
|
||||
ngOnInit () {
|
||||
this.options = Object.keys(this.backupInfo['package-backups']).map(id => {
|
||||
return {
|
||||
...this.backupInfo['package-backups'][id],
|
||||
id,
|
||||
checked: false,
|
||||
installed: !!this.patch.getData()['package-data'][id],
|
||||
'newer-eos': this.emver.compare(this.backupInfo['package-backups'][id]['os-version'], this.config.version) === 1,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
dismiss () {
|
||||
this.modalCtrl.dismiss()
|
||||
}
|
||||
|
||||
handleChange () {
|
||||
this.hasSelection = this.options.some(o => o.checked)
|
||||
}
|
||||
|
||||
async restore (): Promise<void> {
|
||||
const ids = this.options
|
||||
.filter(option => !!option.checked)
|
||||
.map(option => option.id)
|
||||
|
||||
const loader = await this.loadingCtrl.create({
|
||||
spinner: 'lines',
|
||||
message: 'Initializing...',
|
||||
cssClass: 'loader',
|
||||
})
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
await this.embassyApi.restorePackages({
|
||||
ids,
|
||||
'target-id': this.id,
|
||||
'old-password': this.oldPassword,
|
||||
password: this.password,
|
||||
})
|
||||
this.modalCtrl.dismiss(undefined, 'success')
|
||||
} catch (e) {
|
||||
this.error = getErrorMessage(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { BackupReportPage } from './backup-report.page'
|
||||
|
||||
@NgModule({
|
||||
declarations: [BackupReportPage],
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
],
|
||||
exports: [BackupReportPage],
|
||||
})
|
||||
export class BackupReportPageModule { }
|
||||
@@ -0,0 +1,30 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Backup Report</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button (click)="dismiss()">
|
||||
<ion-icon slot="icon-only" name="close"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content>
|
||||
<ion-item-group>
|
||||
<ion-item-divider>Completed: {{ timestamp | date : 'short' }}</ion-item-divider>
|
||||
<ion-item>
|
||||
<ion-label>
|
||||
<h2>System data</h2>
|
||||
<p><ion-text [color]="system.color">{{ system.result }}</ion-text></p>
|
||||
</ion-label>
|
||||
<ion-icon slot="end" [name]="system.icon" [color]="system.color"></ion-icon>
|
||||
</ion-item>
|
||||
<ion-item *ngFor="let pkg of report.packages | keyvalue">
|
||||
<ion-label>
|
||||
<h2>{{ pkg.key }}</h2>
|
||||
<p><ion-text [color]="pkg.value.error ? 'danger' : 'success'">{{ pkg.value.error ? 'Failed: ' + pkg.value.error : 'Succeeded' }}</ion-text></p>
|
||||
</ion-label>
|
||||
<ion-icon slot="end" [name]="pkg.value.error ? 'remove-circle-outline' : 'checkmark'" [color]="pkg.value.error ? 'danger' : 'success'"></ion-icon>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
</ion-content>
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { ModalController } from '@ionic/angular'
|
||||
import { BackupReport } from 'src/app/services/api/api.types'
|
||||
|
||||
@Component({
|
||||
selector: 'backup-report',
|
||||
templateUrl: './backup-report.page.html',
|
||||
styleUrls: ['./backup-report.page.scss'],
|
||||
})
|
||||
export class BackupReportPage {
|
||||
@Input() report: BackupReport
|
||||
@Input() timestamp: string
|
||||
system: {
|
||||
result: string
|
||||
icon: 'remove' | 'remove-circle-outline' | 'checkmark'
|
||||
color: 'dark' | 'danger' | 'success'
|
||||
}
|
||||
|
||||
constructor (
|
||||
private readonly modalCtrl: ModalController,
|
||||
) { }
|
||||
|
||||
ngOnInit () {
|
||||
if (!this.report.server.attempted) {
|
||||
this.system = {
|
||||
result: 'Not Attempted',
|
||||
icon: 'remove',
|
||||
color: 'dark',
|
||||
}
|
||||
} else if (this.report.server.error) {
|
||||
this.system = {
|
||||
result: `Failed: ${this.report.server.error}`,
|
||||
icon: 'remove-circle-outline',
|
||||
color: 'danger',
|
||||
}
|
||||
} else {
|
||||
this.system = {
|
||||
result: 'Succeeded',
|
||||
icon: 'checkmark',
|
||||
color: 'success',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async dismiss () {
|
||||
return this.modalCtrl.dismiss(true)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
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,
|
||||
],
|
||||
exports: [EnumListPage],
|
||||
})
|
||||
export class EnumListPageModule { }
|
||||
@@ -0,0 +1,36 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-button (click)="dismiss()">
|
||||
<ion-icon slot="icon-only" name="close"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
<ion-title>
|
||||
{{ spec.name }}
|
||||
</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button slot="end" fill="clear" (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>{{ spec.spec['value-names'][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="end" class="ion-padding-end">
|
||||
<ion-button fill="outline" (click)="save()" class="enter-click">
|
||||
Done
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-footer>
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { ModalController } from '@ionic/angular'
|
||||
import { ValueSpecListOf } from '../../pkg-config/config-types'
|
||||
|
||||
@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 } = { }
|
||||
selectAll = true
|
||||
|
||||
constructor (
|
||||
private readonly modalCtrl: ModalController,
|
||||
) { }
|
||||
|
||||
ngOnInit () {
|
||||
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
|
||||
}
|
||||
|
||||
toggleSelected (key: string) {
|
||||
this.options[key] = !this.options[key]
|
||||
}
|
||||
|
||||
asIsOrder () {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { GenericFormPage } from './generic-form.page'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { FormObjectComponentModule } from 'src/app/components/form-object/form-object.component.module'
|
||||
|
||||
@NgModule({
|
||||
declarations: [GenericFormPage],
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
FormObjectComponentModule,
|
||||
],
|
||||
exports: [GenericFormPage],
|
||||
})
|
||||
export class GenericFormPageModule { }
|
||||
@@ -0,0 +1,30 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-button (click)="dismiss()">
|
||||
<ion-icon slot="icon-only" name="close"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
<ion-title>{{ title }}</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<form [formGroup]="formGroup" (ngSubmit)="handleClick(submitBtn.handler)" novalidate>
|
||||
<form-object
|
||||
[objectSpec]="spec"
|
||||
[formGroup]="formGroup"
|
||||
></form-object>
|
||||
<button hidden type="submit"></button>
|
||||
</form>
|
||||
</ion-content>
|
||||
|
||||
<ion-footer>
|
||||
<ion-toolbar class="footer">
|
||||
<ion-buttons slot="end">
|
||||
<ion-button class="ion-padding-end" *ngFor="let button of buttons" (click)="handleClick(button.handler)">
|
||||
{{ button.text }}
|
||||
</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);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { FormGroup } from '@angular/forms'
|
||||
import { ModalController } from '@ionic/angular'
|
||||
import { convertValuesRecursive, FormService } from 'src/app/services/form.service'
|
||||
import { ConfigSpec } from 'src/app/pkg-config/config-types'
|
||||
|
||||
export interface ActionButton {
|
||||
text: string
|
||||
handler: (value: any) => Promise<boolean>
|
||||
isSubmit?: boolean
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'generic-form',
|
||||
templateUrl: './generic-form.page.html',
|
||||
styleUrls: ['./generic-form.page.scss'],
|
||||
})
|
||||
export class GenericFormPage {
|
||||
@Input() title: string
|
||||
@Input() spec: ConfigSpec
|
||||
@Input() buttons: ActionButton[]
|
||||
@Input() initialValue: object = { }
|
||||
submitBtn: ActionButton
|
||||
formGroup: FormGroup
|
||||
|
||||
constructor (
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly formService: FormService,
|
||||
) { }
|
||||
|
||||
ngOnInit () {
|
||||
this.formGroup = this.formService.createForm(this.spec, this.initialValue)
|
||||
this.submitBtn = this.buttons.find(btn => btn.isSubmit) || {
|
||||
text: '',
|
||||
handler: () => Promise.resolve(true),
|
||||
}
|
||||
}
|
||||
|
||||
async dismiss (): Promise<void> {
|
||||
this.modalCtrl.dismiss()
|
||||
}
|
||||
|
||||
async handleClick (handler: ActionButton['handler']): Promise<void> {
|
||||
convertValuesRecursive(this.spec, this.formGroup)
|
||||
|
||||
if (this.formGroup.invalid) {
|
||||
this.formGroup.markAllAsTouched()
|
||||
document.getElementsByClassName('validation-error')[0].parentElement.parentElement.scrollIntoView({ behavior: 'smooth' })
|
||||
return
|
||||
}
|
||||
|
||||
// @TODO make this more like generic input component dismissal
|
||||
const success = await handler(this.formGroup.value)
|
||||
if (success !== false) this.modalCtrl.dismiss()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<ion-content>
|
||||
<div style="margin: 24px 24px 12px 24px; display: flex; flex-direction: column;">
|
||||
|
||||
<ion-item style="padding-bottom: 8px;">
|
||||
<ion-label>
|
||||
<h1>{{ options.title }}</h1>
|
||||
<br />
|
||||
<p>{{ options.message }}</p>
|
||||
<ng-container *ngIf="options.warning">
|
||||
<br />
|
||||
<p>
|
||||
<ion-text color="warning">{{ options.warning }}</ion-text>
|
||||
</p>
|
||||
</ng-container>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<form (ngSubmit)="submit()">
|
||||
<div style="margin: 0 0 24px 16px;">
|
||||
<p class="input-label">{{ options.label }}</p>
|
||||
<ion-item lines="none" color="dark">
|
||||
<ion-input #mainInput [type]="options.useMask && !unmasked ? 'password' : 'text'" [(ngModel)]="value" name="value" [placeholder]="options.placeholder" (ionChange)="error = ''"></ion-input>
|
||||
<ion-button slot="end" *ngIf="options.useMask" fill="clear" color="light" (click)="toggleMask()">
|
||||
<ion-icon slot="icon-only" [name]="unmasked ? 'eye-off-outline' : 'eye-outline'" size="small"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
<!-- error -->
|
||||
<p *ngIf="error"><ion-text color="danger">{{ error }}</ion-text></p>
|
||||
</div>
|
||||
|
||||
<div class="ion-text-right">
|
||||
<ion-button fill="clear" (click)="cancel()">
|
||||
Cancel
|
||||
</ion-button>
|
||||
<ion-button fill="clear" type="submit" [disabled]="!value && !options.nullable">
|
||||
{{ options.buttonText }}
|
||||
</ion-button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</ion-content>
|
||||
@@ -0,0 +1,20 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { GenericInputComponent } from './generic-input.component'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
|
||||
@NgModule({
|
||||
declarations: [GenericInputComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
FormsModule,
|
||||
RouterModule.forChild([]),
|
||||
SharingModule,
|
||||
],
|
||||
exports: [GenericInputComponent],
|
||||
})
|
||||
export class GenericInputComponentModule { }
|
||||
@@ -0,0 +1,76 @@
|
||||
import { Component, Input, ViewChild } from '@angular/core'
|
||||
import { ModalController, IonicSafeString, IonInput } from '@ionic/angular'
|
||||
import { getErrorMessage } from 'src/app/services/error-toast.service'
|
||||
|
||||
@Component({
|
||||
selector: 'generic-input',
|
||||
templateUrl: './generic-input.component.html',
|
||||
styleUrls: ['./generic-input.component.scss'],
|
||||
})
|
||||
export class GenericInputComponent {
|
||||
@ViewChild('mainInput') elem: IonInput
|
||||
@Input() options: GenericInputOptions
|
||||
value: string
|
||||
unmasked = false
|
||||
error: string | IonicSafeString
|
||||
|
||||
constructor (
|
||||
private readonly modalCtrl: ModalController,
|
||||
) { }
|
||||
|
||||
ngOnInit () {
|
||||
const defaultOptions: Partial<GenericInputOptions> = {
|
||||
buttonText: 'Submit',
|
||||
placeholder: 'Enter value',
|
||||
nullable: false,
|
||||
useMask: false,
|
||||
initialValue: '',
|
||||
}
|
||||
this.options = {
|
||||
...defaultOptions,
|
||||
...this.options,
|
||||
}
|
||||
|
||||
this.value = this.options.initialValue
|
||||
}
|
||||
|
||||
ngAfterViewInit () {
|
||||
setTimeout(() => this.elem.setFocus(), 400)
|
||||
}
|
||||
|
||||
toggleMask () {
|
||||
this.unmasked = !this.unmasked
|
||||
}
|
||||
|
||||
cancel () {
|
||||
this.modalCtrl.dismiss()
|
||||
}
|
||||
|
||||
async submit () {
|
||||
const value = this.value.trim()
|
||||
|
||||
if (!value && !this.options.nullable) return
|
||||
|
||||
try {
|
||||
await this.options.submitFn(value)
|
||||
this.modalCtrl.dismiss(undefined, 'success')
|
||||
} catch (e) {
|
||||
this.error = getErrorMessage(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface GenericInputOptions {
|
||||
// required
|
||||
title: string
|
||||
message: string
|
||||
label: string
|
||||
submitFn: (value: string) => Promise<any>
|
||||
// optional
|
||||
warning?: string
|
||||
buttonText?: string
|
||||
placeholder?: string
|
||||
nullable?: boolean
|
||||
useMask?: boolean
|
||||
initialValue?: string
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { MarkdownPage } from './markdown.page'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
|
||||
@NgModule({
|
||||
declarations: [MarkdownPage],
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
SharingModule,
|
||||
],
|
||||
exports: [MarkdownPage],
|
||||
})
|
||||
export class MarkdownPageModule { }
|
||||
@@ -0,0 +1,29 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>{{ title | titlecase }}</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button (click)="dismiss()">
|
||||
<ion-icon slot="icon-only" name="close"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content>
|
||||
<text-spinner *ngIf="loading; else loaded" [text]="'Loading ' + title | titlecase"></text-spinner>
|
||||
|
||||
<ng-template #loaded>
|
||||
|
||||
<ion-item *ngIf="loadingError; else noError">
|
||||
<ion-label>
|
||||
<ion-text color="danger">
|
||||
{{ loadingError }}
|
||||
</ion-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ng-template #noError>
|
||||
<div class="content-padding" [innerHTML]="content | markdown"></div>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</ion-content>
|
||||
@@ -0,0 +1,3 @@
|
||||
.content-padding {
|
||||
padding: 0 16px 16px 16px
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { ModalController, IonicSafeString } from '@ionic/angular'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { getErrorMessage } from 'src/app/services/error-toast.service'
|
||||
|
||||
@Component({
|
||||
selector: 'markdown',
|
||||
templateUrl: './markdown.page.html',
|
||||
styleUrls: ['./markdown.page.scss'],
|
||||
})
|
||||
export class MarkdownPage {
|
||||
@Input() contentUrl: string
|
||||
@Input() title: string
|
||||
content: string
|
||||
loading = true
|
||||
loadingError: string | IonicSafeString
|
||||
|
||||
constructor (
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly embassyApi: ApiService,
|
||||
) { }
|
||||
|
||||
async ngOnInit () {
|
||||
try {
|
||||
this.content = await this.embassyApi.getStatic(this.contentUrl)
|
||||
const links = document.links
|
||||
for (let i = 0, linksLength = links.length; i < linksLength; i++) {
|
||||
if (links[i].hostname != window.location.hostname) {
|
||||
links[i].target = '_blank'
|
||||
links[i].setAttribute('rel', 'noreferrer')
|
||||
links[i].className += ' externalLink'
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
this.loadingError = getErrorMessage(e)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
async dismiss () {
|
||||
return this.modalCtrl.dismiss(true)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { OSWelcomePage } from './os-welcome.page'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
|
||||
@NgModule({
|
||||
declarations: [OSWelcomePage],
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
FormsModule,
|
||||
SharingModule,
|
||||
],
|
||||
exports: [OSWelcomePage],
|
||||
})
|
||||
export class OSWelcomePageModule { }
|
||||
@@ -0,0 +1,20 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Welcome to {{ version }}!</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<div style="display: flex; flex-direction: column; justify-content: space-between; height: 100%">
|
||||
<h2>A Whole New Embassy</h2>
|
||||
<div class="main-content">
|
||||
<p>New features and more new features!</p>
|
||||
</div>
|
||||
|
||||
<div class="close-button">
|
||||
<ion-button fill="outline" color="dark" (click)="dismiss()">
|
||||
Begin
|
||||
</ion-button>
|
||||
</div>
|
||||
</div>
|
||||
</ion-content>
|
||||
@@ -0,0 +1,12 @@
|
||||
.close-button {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
height: 100%;
|
||||
color: var(--ion-color-dark);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { ModalController } from '@ionic/angular'
|
||||
|
||||
@Component({
|
||||
selector: 'os-welcome',
|
||||
templateUrl: './os-welcome.page.html',
|
||||
styleUrls: ['./os-welcome.page.scss'],
|
||||
})
|
||||
export class OSWelcomePage {
|
||||
@Input() version: string
|
||||
|
||||
constructor (
|
||||
private readonly modalCtrl: ModalController,
|
||||
) { }
|
||||
|
||||
async dismiss () {
|
||||
return this.modalCtrl.dismiss()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user