diff --git a/apps/showcase/src/components/training/code-editor-view/code-editor-view.component.ts b/apps/showcase/src/components/training/code-editor-view/code-editor-view.component.ts index 6816793dc0..727af0a6fd 100644 --- a/apps/showcase/src/components/training/code-editor-view/code-editor-view.component.ts +++ b/apps/showcase/src/components/training/code-editor-view/code-editor-view.component.ts @@ -10,6 +10,7 @@ import { inject, input, OnDestroy, + OnInit, untracked, ViewChild, ViewEncapsulation, @@ -25,6 +26,9 @@ import { FormsModule, ReactiveFormsModule, } from '@angular/forms'; +import { + DfModalService, +} from '@design-factory/design-factory'; import { LoggerService, } from '@o3r/logger'; @@ -44,6 +48,7 @@ import { } from 'ngx-monaco-tree'; import { BehaviorSubject, + combineLatest, combineLatestWith, debounceTime, distinctUntilChanged, @@ -51,9 +56,11 @@ import { firstValueFrom, from, map, + merge, Observable, of, share, + shareReplay, skip, startWith, Subject, @@ -69,6 +76,9 @@ import { import { CodeEditorControlComponent, } from '../code-editor-control'; +import { + SaveCodeDialogComponent, +} from '../save-code-dialog'; declare global { interface Window { @@ -117,7 +127,7 @@ export interface TrainingProject { templateUrl: './code-editor-view.component.html', styleUrl: './code-editor-view.component.scss' }) -export class CodeEditorViewComponent implements OnDestroy { +export class CodeEditorViewComponent implements OnDestroy, OnInit { /** * @see {FormBuilder} */ @@ -163,7 +173,7 @@ export class CodeEditorViewComponent implements OnDestroy { : of([]) ), filter((tree) => tree.length > 0), - share() + shareReplay({ bufferSize: 1, refCount: true }) ); /** @@ -205,11 +215,18 @@ export class CodeEditorViewComponent implements OnDestroy { })) ); - private readonly fileContentLoaded$ = this.form.controls.file.valueChanges.pipe( + private readonly modalService = inject(DfModalService); + private readonly forceReload = new Subject(); + private readonly forceSave = new Subject(); + + private readonly fileContentLoaded$ = combineLatest([ + this.form.controls.file.valueChanges, + this.forceReload.pipe(startWith(undefined)) + ]).pipe( takeUntilDestroyed(), combineLatestWith(this.cwdTree$), - filter(([path, monacoTree]) => !!path && checkIfPathInMonacoTree(monacoTree, path.split('/'))), - switchMap(([path]) => from(this.webContainerService.readFile(`${this.project().cwd}/${path}`).catch(() => ''))), + filter(([[path], monacoTree]) => !!path && checkIfPathInMonacoTree(monacoTree, path.split('/'))), + switchMap(([[path]]) => from(this.webContainerService.readFile(`${this.project().cwd}/${path}`).catch(() => ''))), share() ); @@ -238,14 +255,22 @@ export class CodeEditorViewComponent implements OnDestroy { // Remove link between launch project and terminals await this.webContainerService.loadProject(project.files, project.commands, project.cwd); } - await this.loadNewProject(); - this.cwd$.next(project?.cwd || ''); + this.loadNewProject(); + this.cwd$.next(project.cwd); }); }); - this.form.controls.code.valueChanges.pipe( - distinctUntilChanged(), - skip(1), - debounceTime(300), + this.forceReload.subscribe(async () => { + await this.cleanAllModelsFromMonaco(); + await this.loadAllProjectFilesToMonaco(); + }); + merge( + this.forceSave.pipe(map(() => this.form.value.code)), + this.form.controls.code.valueChanges.pipe( + distinctUntilChanged(), + skip(1), + debounceTime(1000) + ) + ).pipe( filter((text): text is string => !!text), takeUntilDestroyed() ).subscribe((text: string) => { @@ -253,11 +278,20 @@ export class CodeEditorViewComponent implements OnDestroy { this.loggerService.error('No project found'); return; } + if (text !== this.fileContent()) { + const { cwd } = this.project(); + localStorage.setItem(cwd, JSON.stringify({ + ...JSON.parse(localStorage.getItem(cwd) || '{}'), + [this.form.controls.file.value!]: text + })); + } const path = `${this.project().cwd}/${this.form.controls.file.value}`; this.loggerService.log('Writing file', path); void this.webContainerService.writeFile(path, text); }); - this.fileContentLoaded$.subscribe((content) => this.form.controls.code.setValue(content)); + this.fileContentLoaded$.subscribe((content) => { + this.form.controls.code.setValue(content); + }); // Reload definition types when finishing install this.webContainerService.runner.dependenciesLoaded$.pipe( @@ -338,15 +372,9 @@ export class CodeEditorViewComponent implements OnDestroy { /** * Load a new project in global monaco editor and update local form accordingly */ - private async loadNewProject() { - if (this.project()?.startingFile) { - this.form.controls.file.setValue(this.project().startingFile); - } else { - this.form.controls.file.setValue(''); - this.form.controls.code.setValue(''); - } - await this.cleanAllModelsFromMonaco(); - await this.loadAllProjectFilesToMonaco(); + private loadNewProject() { + this.form.controls.file.setValue(this.project().startingFile); + this.forceReload.next(); } /** @@ -367,6 +395,7 @@ export class CodeEditorViewComponent implements OnDestroy { public onEditorKeyDown(event: KeyboardEvent) { const ctrlKey = /mac/i.test(navigator.userAgent) ? event.metaKey : event.ctrlKey; if (ctrlKey && event.key.toLowerCase() === 's') { + this.forceSave.next(); event.stopPropagation(); event.preventDefault(); } @@ -382,10 +411,32 @@ export class CodeEditorViewComponent implements OnDestroy { } } + public ngOnInit() { + const { cwd } = this.project(); + const savedState = localStorage.getItem(cwd); + if (savedState) { + const modal = this.modalService.open(SaveCodeDialogComponent, { backdrop: 'static' }); + void modal.result.then(async (positiveReply) => { + if (positiveReply) { + await firstValueFrom(this.cwdTree$); + const state = JSON.parse(savedState); + await Promise.all(Object.entries(state) + .map(([path, text]) => this.webContainerService.writeFile(`${cwd}/${path}`, text))); + this.forceReload.next(); + } else { + localStorage.removeItem(cwd); + } + }); + } + } + /** * @inheritDoc */ public ngOnDestroy() { + this.forceReload.complete(); + this.forceSave.complete(); + this.newMonacoEditorCreated.complete(); this.webContainerService.runner.killContainer(); } } diff --git a/apps/showcase/src/components/training/save-code-dialog/index.ts b/apps/showcase/src/components/training/save-code-dialog/index.ts new file mode 100644 index 0000000000..aeaabc8013 --- /dev/null +++ b/apps/showcase/src/components/training/save-code-dialog/index.ts @@ -0,0 +1 @@ +export * from './save-code-dialog.component'; diff --git a/apps/showcase/src/components/training/save-code-dialog/save-code-dialog.component.spec.ts b/apps/showcase/src/components/training/save-code-dialog/save-code-dialog.component.spec.ts new file mode 100644 index 0000000000..33558e6bfc --- /dev/null +++ b/apps/showcase/src/components/training/save-code-dialog/save-code-dialog.component.spec.ts @@ -0,0 +1,26 @@ +import { + ComponentFixture, + TestBed, +} from '@angular/core/testing'; +import { + SaveCodeDialogComponent, +} from './save-code-dialog.component'; + +describe('ViewComponent', () => { + let component: SaveCodeDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SaveCodeDialogComponent] + }).compileComponents(); + + fixture = TestBed.createComponent(SaveCodeDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/apps/showcase/src/components/training/save-code-dialog/save-code-dialog.component.ts b/apps/showcase/src/components/training/save-code-dialog/save-code-dialog.component.ts new file mode 100644 index 0000000000..9fc894a659 --- /dev/null +++ b/apps/showcase/src/components/training/save-code-dialog/save-code-dialog.component.ts @@ -0,0 +1,19 @@ +import { + ChangeDetectionStrategy, + Component, + inject, +} from '@angular/core'; +import { + NgbActiveModal, +} from '@ng-bootstrap/ng-bootstrap'; + +@Component({ + selector: 'code-editor-terminal', + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [], + templateUrl: './save-code-dialog.template.html' +}) +export class SaveCodeDialogComponent { + public readonly activeModal = inject(NgbActiveModal); +} diff --git a/apps/showcase/src/components/training/save-code-dialog/save-code-dialog.template.html b/apps/showcase/src/components/training/save-code-dialog/save-code-dialog.template.html new file mode 100644 index 0000000000..f9a084ead7 --- /dev/null +++ b/apps/showcase/src/components/training/save-code-dialog/save-code-dialog.template.html @@ -0,0 +1,10 @@ + + +