Skip to content

Commit

Permalink
feat(training): persistence of code modifications
Browse files Browse the repository at this point in the history
  • Loading branch information
matthieu-crouzet committed Dec 13, 2024
1 parent 74e8403 commit 2c4e518
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
inject,
input,
OnDestroy,
OnInit,
untracked,
ViewChild,
ViewEncapsulation,
Expand All @@ -25,6 +26,9 @@ import {
FormsModule,
ReactiveFormsModule,
} from '@angular/forms';
import {
DfModalService,
} from '@design-factory/design-factory';
import {
LoggerService,
} from '@o3r/logger';
Expand All @@ -44,16 +48,19 @@ import {
} from 'ngx-monaco-tree';
import {
BehaviorSubject,
combineLatest,
combineLatestWith,
debounceTime,
distinctUntilChanged,
filter,
firstValueFrom,
from,
map,
merge,
Observable,
of,
share,
shareReplay,
skip,
startWith,
Subject,
Expand All @@ -69,6 +76,9 @@ import {
import {
CodeEditorControlComponent,
} from '../code-editor-control';
import {
SaveCodeDialogComponent,
} from '../save-code-dialog';

declare global {
interface Window {
Expand Down Expand Up @@ -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}
*/
Expand Down Expand Up @@ -163,7 +173,7 @@ export class CodeEditorViewComponent implements OnDestroy {
: of([])
),
filter((tree) => tree.length > 0),
share()
shareReplay({ bufferSize: 1, refCount: true })
);

/**
Expand Down Expand Up @@ -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<void>();
private readonly forceSave = new Subject<void>();

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()
);

Expand Down Expand Up @@ -238,26 +255,43 @@ 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) => {
if (!this.project) {
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(
Expand Down Expand Up @@ -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();
}

/**
Expand All @@ -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();
}
Expand All @@ -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<string>(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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './save-code-dialog.component';
Original file line number Diff line number Diff line change
@@ -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<SaveCodeDialogComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [SaveCodeDialogComponent]
}).compileComponents();

fixture = TestBed.createComponent(SaveCodeDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<div class="modal-header">
<h2 class="modal-title">Code modifications detected</h2>
</div>
<div class="modal-body">
<p>Do you want to keep your previous changes or discard them?</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-danger me-5" (click)="activeModal.close(false)">Discard</button>
<button type="button" class="btn btn-primary" ngbAutofocus (click)="activeModal.close(true)">Keep</button>
</div>

0 comments on commit 2c4e518

Please sign in to comment.