diff --git a/frontend/projects/ui/src/app/app.component.html b/frontend/projects/ui/src/app/app.component.html index 5c5419de8..8b0d955a4 100644 --- a/frontend/projects/ui/src/app/app.component.html +++ b/frontend/projects/ui/src/app/app.component.html @@ -185,8 +185,6 @@ - - diff --git a/frontend/projects/ui/src/app/pages/developer-routes/dev-config/dev-config.page.html b/frontend/projects/ui/src/app/pages/developer-routes/dev-config/dev-config.page.html index 38452d3b4..688639533 100644 --- a/frontend/projects/ui/src/app/pages/developer-routes/dev-config/dev-config.page.html +++ b/frontend/projects/ui/src/app/pages/developer-routes/dev-config/dev-config.page.html @@ -1,17 +1,27 @@ - + - Config + Config + - Preview + Preview diff --git a/frontend/projects/ui/src/app/pages/developer-routes/dev-config/dev-config.page.scss b/frontend/projects/ui/src/app/pages/developer-routes/dev-config/dev-config.page.scss index e69de29bb..af1932671 100644 --- a/frontend/projects/ui/src/app/pages/developer-routes/dev-config/dev-config.page.scss +++ b/frontend/projects/ui/src/app/pages/developer-routes/dev-config/dev-config.page.scss @@ -0,0 +1,7 @@ +.centered{ + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + transform: scale(2); +} \ No newline at end of file diff --git a/frontend/projects/ui/src/app/pages/developer-routes/dev-config/dev-config.page.ts b/frontend/projects/ui/src/app/pages/developer-routes/dev-config/dev-config.page.ts index 59cc86a81..f0cdae299 100644 --- a/frontend/projects/ui/src/app/pages/developer-routes/dev-config/dev-config.page.ts +++ b/frontend/projects/ui/src/app/pages/developer-routes/dev-config/dev-config.page.ts @@ -1,8 +1,12 @@ import { Component } from '@angular/core' +import { ActivatedRoute } from '@angular/router' import { ModalController } from '@ionic/angular' import * as yaml from 'js-yaml' +import { take } from 'rxjs/operators' +import { ApiService } from 'src/app/services/api/embassy-api.service' +import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' +import { debounce } from '../../../../../../shared/src/util/misc.util' import { GenericFormPage } from '../../../modals/generic-form/generic-form.page' -import { ConfigSpec } from '../../../pkg-config/config-types' import { ErrorToastService } from '../../../services/error-toast.service' @Component({ @@ -11,21 +15,31 @@ import { ErrorToastService } from '../../../services/error-toast.service' styleUrls: ['dev-config.page.scss'], }) export class DevConfigPage { + projectId: string editorOptions = { theme: 'vs-dark', language: 'yaml' } - code: string + code: string = '' + saving: boolean = false constructor( + private readonly route: ActivatedRoute, private readonly errToast: ErrorToastService, private readonly modalCtrl: ModalController, + private readonly patchDb: PatchDbService, + private readonly api: ApiService, ) {} ngOnInit() { - this.code = yaml - .dump(SAMPLE_CODE) - .replace(/warning:/g, '# Optional\n warning:') + this.projectId = this.route.snapshot.paramMap.get('projectId') + + this.patchDb + .watch$('ui', 'dev', this.projectId, 'config') + .pipe(take(1)) + .subscribe(config => { + this.code = config + }) } - async submit() { + async preview() { let doc: any try { doc = yaml.load(this.code) @@ -51,56 +65,21 @@ export class DevConfigPage { }) await modal.present() } -} -const SAMPLE_CODE: ConfigSpec = { - 'sample-string': { - type: 'string', - name: 'Example String Input', - nullable: false, - masked: false, - copyable: false, - // optional - warning: null, - description: 'Example description for required string input.', - default: null, - placeholder: 'Enter string value', - pattern: '^[a-zA-Z0-9! _]+$', - 'pattern-description': 'Must be alphanumeric (may contain underscore).', - }, - 'sample-number': { - type: 'number', - name: 'Example Number Input', - nullable: false, - range: '[5,1000000]', - integral: true, - // optional - warning: 'Example warning to display when changing this number value.', - units: 'ms', - description: 'Example description for optional number input.', - default: null, - placeholder: 'Enter number value', - }, - 'sample-boolean': { - type: 'boolean', - name: 'Example Boolean Toggle', - // optional - warning: null, - description: 'Example description for boolean toggle', - default: true, - }, - 'sample-enum': { - type: 'enum', - name: 'Example Enum Select', - values: ['red', 'blue', 'green'], - 'value-names': { - red: 'Red', - blue: 'Blue', - green: 'Green', - }, - // optional - warning: 'Example warning to display when changing this enum value.', - description: 'Example description for enum select', - default: 'red', - }, + @debounce(1000) + async save() { + this.saving = true + try { + await this.api.setDbValue({ + pointer: `/dev/${this.projectId}/config`, + value: this.code, + }) + } catch (e) { + this.errToast.present({ + message: 'Auto save error: Your changes are not saved.', + } as any) + } finally { + this.saving = false + } + } } diff --git a/frontend/projects/ui/src/app/pages/developer-routes/dev-instructions/dev-instructions.page.html b/frontend/projects/ui/src/app/pages/developer-routes/dev-instructions/dev-instructions.page.html index 475e1b759..6d8bee796 100644 --- a/frontend/projects/ui/src/app/pages/developer-routes/dev-instructions/dev-instructions.page.html +++ b/frontend/projects/ui/src/app/pages/developer-routes/dev-instructions/dev-instructions.page.html @@ -1,17 +1,27 @@ - + - Instructions + Instructions + - Preview + Preview diff --git a/frontend/projects/ui/src/app/pages/developer-routes/dev-instructions/dev-instructions.page.ts b/frontend/projects/ui/src/app/pages/developer-routes/dev-instructions/dev-instructions.page.ts index 67d08a1b1..a875bf9ef 100644 --- a/frontend/projects/ui/src/app/pages/developer-routes/dev-instructions/dev-instructions.page.ts +++ b/frontend/projects/ui/src/app/pages/developer-routes/dev-instructions/dev-instructions.page.ts @@ -1,5 +1,11 @@ import { Component } from '@angular/core' +import { ActivatedRoute } from '@angular/router' import { ModalController } from '@ionic/angular' +import { take } from 'rxjs/operators' +import { ApiService } from 'src/app/services/api/embassy-api.service' +import { ErrorToastService } from 'src/app/services/error-toast.service' +import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' +import { debounce } from '../../../../../../shared/src/util/misc.util' import { MarkdownPage } from '../../../modals/markdown/markdown.page' @Component({ @@ -8,12 +14,31 @@ import { MarkdownPage } from '../../../modals/markdown/markdown.page' styleUrls: ['dev-instructions.page.scss'], }) export class DevInstructionsPage { + projectId: string editorOptions = { theme: 'vs-dark', language: 'markdown' } - code: string = `# Create Instructions using Markdown! :)` + code: string = '' + saving: boolean = false - constructor(private readonly modalCtrl: ModalController) {} + constructor( + private readonly route: ActivatedRoute, + private readonly errToast: ErrorToastService, + private readonly modalCtrl: ModalController, + private readonly patchDb: PatchDbService, + private readonly api: ApiService, + ) {} - async submit() { + ngOnInit() { + this.projectId = this.route.snapshot.paramMap.get('projectId') + + this.patchDb + .watch$('ui', 'dev', this.projectId, 'instructions') + .pipe(take(1)) + .subscribe(config => { + this.code = config + }) + } + + async preview() { const modal = await this.modalCtrl.create({ componentProps: { title: 'Instructions Sample', @@ -24,4 +49,21 @@ export class DevInstructionsPage { await modal.present() } + + @debounce(1000) + async save() { + this.saving = true + try { + await this.api.setDbValue({ + pointer: `/dev/${this.projectId}/instructions`, + value: this.code, + }) + } catch (e) { + this.errToast.present({ + message: 'Auto save error: Your changes are not saved.', + } as any) + } finally { + this.saving = false + } + } } diff --git a/frontend/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.module.ts b/frontend/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.module.ts index edb90709a..ea0c78423 100644 --- a/frontend/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.module.ts +++ b/frontend/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.module.ts @@ -2,14 +2,14 @@ import { NgModule } from '@angular/core' import { CommonModule } from '@angular/common' import { IonicModule } from '@ionic/angular' import { RouterModule, Routes } from '@angular/router' -import { DeveloperPage } from './developer-list.page' +import { DeveloperListPage } from './developer-list.page' import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module' import { BackupReportPageModule } from 'src/app/modals/backup-report/backup-report.module' const routes: Routes = [ { path: '', - component: DeveloperPage, + component: DeveloperListPage, }, ] @@ -21,6 +21,6 @@ const routes: Routes = [ BadgeMenuComponentModule, BackupReportPageModule, ], - declarations: [DeveloperPage], + declarations: [DeveloperListPage], }) -export class DeveloperPageModule {} +export class DeveloperListPageModule {} diff --git a/frontend/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.page.html b/frontend/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.page.html index 89821961d..e580457dc 100644 --- a/frontend/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.page.html +++ b/frontend/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.page.html @@ -4,23 +4,36 @@ Developer Tools + + + - Components - - + Projects + + + -

Instructions Generator

-

Create instructions and see how they will appear to the end user

+ Create project
- - - -

Config Generator

-

Edit the config with YAML and see it in real time

-
+ + +

{{ entry.value.name }}

+ + + Delete +
diff --git a/frontend/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.page.ts b/frontend/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.page.ts index 394359f1d..2d034b999 100644 --- a/frontend/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.page.ts +++ b/frontend/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.page.ts @@ -1,23 +1,211 @@ import { Component } from '@angular/core' +import { + AlertController, + LoadingController, + ModalController, + NavController, +} from '@ionic/angular' +import { + GenericInputComponent, + GenericInputOptions, +} from 'src/app/modals/generic-input/generic-input.component' +import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' +import { ApiService } from 'src/app/services/api/embassy-api.service' +import { ConfigSpec } from 'src/app/pkg-config/config-types' +import * as yaml from 'js-yaml' +import { v4 } from 'uuid' +import { DevData } from 'src/app/services/patch-db/data-model' +import { Subscription } from 'rxjs' +import { ErrorToastService } from 'src/app/services/error-toast.service' import { ActivatedRoute } from '@angular/router' -import { NavController } from '@ionic/angular' +import { DestroyService } from '@start9labs/shared' +import { takeUntil } from 'rxjs/operators' @Component({ selector: 'developer-list', templateUrl: 'developer-list.page.html', styleUrls: ['developer-list.page.scss'], + providers: [DestroyService], }) -export class DeveloperPage { +export class DeveloperListPage { + devData: DevData + constructor( + private readonly modalCtrl: ModalController, + private readonly api: ApiService, + private readonly loadingCtrl: LoadingController, + private readonly errToast: ErrorToastService, + private readonly alertCtrl: AlertController, private readonly navCtrl: NavController, private readonly route: ActivatedRoute, + private readonly destroy$: DestroyService, + private readonly patch: PatchDbService, ) {} - navToConfig() { - this.navCtrl.navigateForward(['config'], { relativeTo: this.route }) + ngOnInit() { + this.patch + .watch$('ui', 'dev') + .pipe(takeUntil(this.destroy$)) + .subscribe(dd => { + this.devData = dd + }) } - navToInstructions() { - this.navCtrl.navigateForward(['instructions'], { relativeTo: this.route }) + async openCreateProjectModal() { + const projNumber = Object.keys(this.devData || {}).length + 1 + const options: GenericInputOptions = { + title: 'Add new project', + message: 'Create a new dev project.', + label: 'New project', + useMask: false, + placeholder: `Project ${projNumber}`, + nullable: true, + initialValue: `Project ${projNumber}`, + buttonText: 'Save', + submitFn: (value: string) => this.createProject(value), + } + + const modal = await this.modalCtrl.create({ + componentProps: { options }, + cssClass: 'alertlike-modal', + presentingElement: await this.modalCtrl.getTop(), + component: GenericInputComponent, + }) + + await modal.present() + } + + async createProject(name: string) { + // fail silently if duplicate project name + if ( + Object.values(this.devData || {}) + .map(v => v.name) + .includes(name) + ) + return + + const loader = await this.loadingCtrl.create({ + spinner: 'lines', + message: 'Creating Project...', + cssClass: 'loader', + }) + await loader.present() + + try { + const id = v4() + const config = yaml + .dump(SAMPLE_CONFIG) + .replace(/warning:/g, '# Optional\n warning:') + + const def = { name, config, instructions: SAMPLE_INSTUCTIONS } + if (this.devData) { + await this.api.setDbValue({ pointer: `/dev/${id}`, value: def }) + } else { + await this.api.setDbValue({ pointer: `/dev`, value: { [id]: def } }) + } + } catch (e) { + this.errToast.present({ message: `Error saving project data` } as any) + } finally { + loader.dismiss() + } + } + + async presentAlertDelete(id: string, event: Event) { + event.stopPropagation() + const alert = await this.alertCtrl.create({ + header: 'Caution', + message: `Are you sure you want to delete this project?`, + buttons: [ + { + text: 'Cancel', + role: 'cancel', + }, + { + text: 'Delete', + handler: () => { + this.delete(id) + }, + cssClass: 'enter-click', + }, + ], + }) + await alert.present() + } + + async delete(id: string) { + const loader = await this.loadingCtrl.create({ + spinner: 'lines', + message: 'Removing Project...', + cssClass: 'loader', + }) + await loader.present() + + try { + const devDataToSave: DevData = JSON.parse(JSON.stringify(this.devData)) + delete devDataToSave[id] + await this.api.setDbValue({ pointer: `/dev`, value: devDataToSave }) + } catch (e) { + this.errToast.present({ message: `Error deleting project data` } as any) + } finally { + loader.dismiss() + } + } + + async goToProject(id: string) { + await this.navCtrl.navigateForward([id], { relativeTo: this.route }) } } + +const SAMPLE_INSTUCTIONS = `# Create Instructions using Markdown! :)` + +const SAMPLE_CONFIG: ConfigSpec = { + 'sample-string': { + type: 'string', + name: 'Example String Input', + nullable: false, + masked: false, + copyable: false, + // optional + warning: null, + description: 'Example description for required string input.', + default: null, + placeholder: 'Enter string value', + pattern: '^[a-zA-Z0-9! _]+$', + 'pattern-description': 'Must be alphanumeric (may contain underscore).', + }, + 'sample-number': { + type: 'number', + name: 'Example Number Input', + nullable: false, + range: '[5,1000000]', + integral: true, + // optional + warning: 'Example warning to display when changing this number value.', + units: 'ms', + description: 'Example description for optional number input.', + default: null, + placeholder: 'Enter number value', + }, + 'sample-boolean': { + type: 'boolean', + name: 'Example Boolean Toggle', + // optional + warning: null, + description: 'Example description for boolean toggle', + default: true, + }, + 'sample-enum': { + type: 'enum', + name: 'Example Enum Select', + values: ['red', 'blue', 'green'], + 'value-names': { + red: 'Red', + blue: 'Blue', + green: 'Green', + }, + // optional + warning: 'Example warning to display when changing this enum value.', + description: 'Example description for enum select', + default: 'red', + }, +} diff --git a/frontend/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.module.ts b/frontend/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.module.ts new file mode 100644 index 000000000..dd641d94b --- /dev/null +++ b/frontend/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.module.ts @@ -0,0 +1,26 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { IonicModule } from '@ionic/angular' +import { RouterModule, Routes } from '@angular/router' +import { DeveloperMenuPage } from './developer-menu.page' +import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module' +import { BackupReportPageModule } from 'src/app/modals/backup-report/backup-report.module' + +const routes: Routes = [ + { + path: '', + component: DeveloperMenuPage, + }, +] + +@NgModule({ + imports: [ + CommonModule, + IonicModule, + RouterModule.forChild(routes), + BadgeMenuComponentModule, + BackupReportPageModule, + ], + declarations: [DeveloperMenuPage], +}) +export class DeveloperMenuPageModule {} diff --git a/frontend/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.page.html b/frontend/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.page.html new file mode 100644 index 000000000..f0a9c68d5 --- /dev/null +++ b/frontend/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.page.html @@ -0,0 +1,26 @@ + + + + + + {{ patchDb.data.ui.dev[projectId].name}} + + + + + Components + + + +

Instructions Generator

+

Create instructions and see how they will appear to the end user

+
+
+ + + +

Config Generator

+

Edit the config with YAML and see it in real time

+
+
+
diff --git a/frontend/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.page.scss b/frontend/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.page.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.page.ts b/frontend/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.page.ts new file mode 100644 index 000000000..6c7adba4e --- /dev/null +++ b/frontend/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.page.ts @@ -0,0 +1,20 @@ +import { Component } from '@angular/core' +import { ActivatedRoute } from '@angular/router' +import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' + +@Component({ + selector: 'developer-menu', + templateUrl: 'developer-menu.page.html', + styleUrls: ['developer-menu.page.scss'], +}) +export class DeveloperMenuPage { + projectId: string + constructor( + private readonly route: ActivatedRoute, + public readonly patchDb: PatchDbService, + ) {} + + ngOnInit() { + this.projectId = this.route.snapshot.paramMap.get('projectId') + } +} diff --git a/frontend/projects/ui/src/app/pages/developer-routes/developer-routing.module.ts b/frontend/projects/ui/src/app/pages/developer-routes/developer-routing.module.ts index ec2ef2dcf..d2e34dae0 100644 --- a/frontend/projects/ui/src/app/pages/developer-routes/developer-routing.module.ts +++ b/frontend/projects/ui/src/app/pages/developer-routes/developer-routing.module.ts @@ -4,18 +4,30 @@ import { Routes, RouterModule } from '@angular/router' const routes: Routes = [ { path: '', + redirectTo: 'projects', + pathMatch: 'full', + }, + { + path: 'projects', loadChildren: () => import('./developer-list/developer-list.module').then( - m => m.DeveloperPageModule, + m => m.DeveloperListPageModule, ), }, { - path: 'config', + path: 'projects/:projectId', + loadChildren: () => + import('./developer-menu/developer-menu.module').then( + m => m.DeveloperMenuPageModule, + ), + }, + { + path: 'projects/:projectId/config', loadChildren: () => import('./dev-config/dev-config.module').then(m => m.DevConfigPageModule), }, { - path: 'instructions', + path: 'projects/:projectId/instructions', loadChildren: () => import('./dev-instructions/dev-instructions.module').then( m => m.DevInstructionsPageModule, diff --git a/frontend/projects/ui/src/app/services/api/mock-patch.ts b/frontend/projects/ui/src/app/services/api/mock-patch.ts index 4ec4962ea..06c548334 100644 --- a/frontend/projects/ui/src/app/services/api/mock-patch.ts +++ b/frontend/projects/ui/src/app/services/api/mock-patch.ts @@ -15,6 +15,7 @@ export const mockPatchData: DataModel = { 'pkg-order': [], 'ack-welcome': '1.0.0', marketplace: undefined, + dev: undefined, }, 'server-info': { id: 'embassy-abcdefgh', diff --git a/frontend/projects/ui/src/app/services/patch-db/data-model.ts b/frontend/projects/ui/src/app/services/patch-db/data-model.ts index d3a483942..9654bcecf 100644 --- a/frontend/projects/ui/src/app/services/patch-db/data-model.ts +++ b/frontend/projects/ui/src/app/services/patch-db/data-model.ts @@ -14,6 +14,7 @@ export interface UIData { 'pkg-order': string[] 'ack-welcome': string // EOS version marketplace: UIMarketplaceData + dev: DevData } export interface UIMarketplaceData { @@ -26,6 +27,14 @@ export interface UIMarketplaceData { } } +export interface DevData { + [id: string]: { + name: string + instructions?: string + config?: string + } +} + export interface ServerInfo { id: string version: string