Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
4d1eb05
make problem statement as a selectable file in the editor
HawKhiem Aug 31, 2025
00d6469
fix client styles
HawKhiem Aug 31, 2025
b0fc044
fix client styles and client tests
HawKhiem Aug 31, 2025
fd92600
add another test to file browser
HawKhiem Aug 31, 2025
878ac99
Merge branch 'develop' into feature/programming-exercises/problem-sta…
HawKhiem Sep 14, 2025
ab391ea
fix buidl fail
HawKhiem Sep 14, 2025
af2e6ca
fix some existing issues
HawKhiem Sep 14, 2025
2415978
fix some stuffs
HawKhiem Sep 16, 2025
14b9064
resolve merge conflict
HawKhiem Sep 16, 2025
a10c465
address comments
HawKhiem Sep 16, 2025
8271dbe
resolve some issues
HawKhiem Sep 16, 2025
08e13b6
fix build fail
HawKhiem Sep 16, 2025
431d827
Merge branch 'develop' into feature/programming-exercises/problem-sta…
HawKhiem Sep 17, 2025
95ef268
fix client tests
HawKhiem Sep 17, 2025
a415256
address comments
HawKhiem Sep 18, 2025
dfa30f5
implement coderabbit suggestion
HawKhiem Sep 19, 2025
5a57d6b
add 2 more tests
HawKhiem Sep 19, 2025
693e010
implement code rabbit suggestion
HawKhiem Sep 19, 2025
0c7a333
implement code rabbit suggestion
HawKhiem Sep 19, 2025
3ebe9d4
fix failing test
HawKhiem Sep 19, 2025
d80e164
Merge branch 'develop' into feature/programming-exercises/problem-sta…
HawKhiem Sep 19, 2025
bd77120
Merge branch 'develop' into feature/programming-exercises/problem-sta…
HawKhiem Sep 19, 2025
624efce
implement Felix' comments
HawKhiem Sep 24, 2025
d9ffdb6
Merge branch 'develop' into feature/programming-exercises/problem-sta…
HawKhiem Sep 24, 2025
5b422d8
update threshold
HawKhiem Sep 24, 2025
fa94aed
Merge branch 'develop' into feature/programming-exercises/problem-sta…
krusche Oct 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ <h3 class="text-align-left fw-normal">
@if (exercise().allowOnlineEditor) {
<div>
<jhi-code-editor-container
[forRepositoryView]="true"
[editable]="!repositoryIsLocked"
[participation]="studentParticipation()"
[showEditorInstructions]="showEditorInstructions"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
<ng-template #assessment>
@if (!loadingParticipation && !participationCouldNotBeFetched) {
<jhi-code-editor-container
[forRepositoryView]="true"
[editable]="false"
[participation]="participation"
[feedbackSuggestions]="feedbackSuggestions"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,30 +45,44 @@ <h4 class="editor-title"><ng-content select="[editorTitle]" /></h4>
(onToggleCollapse)="onToggleCollapse($event, CollapsableCodeEditorElement.FileBrowser)"
/>
<ng-container editorCenter>
<jhi-code-editor-monaco
[commitState]="commitState"
[editorState]="editorState"
[course]="course"
[feedbacks]="feedbackForSubmission()"
[feedbackSuggestions]="feedbackSuggestions"
[readOnlyManualFeedback]="readOnlyManualFeedback"
[disableActions]="!editable"
[isTutorAssessment]="isTutorAssessment"
[highlightDifferences]="highlightDifferences"
[selectedFile]="selectedFile"
[buildAnnotations]="annotations"
[sessionId]="participation?.id ?? 'test'"
(onFileContentChange)="onFileContentChange($event)"
(onUpdateFeedback)="onUpdateFeedback.emit($event)"
(onAcceptSuggestion)="onAcceptSuggestion.emit($event)"
(onDiscardSuggestion)="onDiscardSuggestion.emit($event)"
(onError)="onError($event)"
(onFileLoad)="fileLoad($event)"
/>
@if (selectedFile === PROBLEM_STATEMENT_IDENTIFIER && showEditorInstructions) {
<jhi-code-editor-instructions
(onToggleCollapse)="onToggleCollapse($event, CollapsableCodeEditorElement.Instructions)"
(onError)="onError($event)"
[isAssessmentMode]="isTutorAssessment"
[disableCollapse]="true"
[isEditor]="true"
>
<ng-content select="[editorCenter]" />
</jhi-code-editor-instructions>
} @else {
<!-- Regular Code Editor -->
<jhi-code-editor-monaco
[commitState]="commitState"
[editorState]="editorState"
[course]="course"
[feedbacks]="feedbackForSubmission()"
[feedbackSuggestions]="feedbackSuggestions"
[readOnlyManualFeedback]="readOnlyManualFeedback"
[disableActions]="!editable"
[isTutorAssessment]="isTutorAssessment"
[highlightDifferences]="highlightDifferences"
[selectedFile]="selectedFile"
[buildAnnotations]="annotations"
[sessionId]="participation?.id ?? 'test'"
(onFileContentChange)="onFileContentChange($event)"
(onUpdateFeedback)="onUpdateFeedback.emit($event)"
(onAcceptSuggestion)="onAcceptSuggestion.emit($event)"
(onDiscardSuggestion)="onDiscardSuggestion.emit($event)"
(onError)="onError($event)"
(onFileLoad)="fileLoad($event)"
/>
}
</ng-container>
<ng-container editorSidebarRight>
@if (showEditorInstructions) {
<jhi-code-editor-instructions
#rightInstructions
(onToggleCollapse)="onToggleCollapse($event, CollapsableCodeEditorElement.Instructions)"
(onError)="onError($event)"
[isAssessmentMode]="isTutorAssessment"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ export class CodeEditorContainerComponent implements OnChanges, ComponentCanDeac
readonly CommitState = CommitState;
readonly EditorState = EditorState;
readonly CollapsableCodeEditorElement = CollapsableCodeEditorElement;
readonly PROBLEM_STATEMENT_IDENTIFIER = '__problem_statement__';
@ViewChild('rightInstructions', { static: false }) rightInstructions: CodeEditorInstructionsComponent;
@ViewChild(CodeEditorGridComponent, { static: false }) grid: CodeEditorGridComponent;

@ViewChild(CodeEditorFileBrowserComponent, { static: false }) fileBrowser: CodeEditorFileBrowserComponent;
Expand Down Expand Up @@ -111,9 +113,16 @@ export class CodeEditorContainerComponent implements OnChanges, ComponentCanDeac
/** END WIP */

// WARNING: Don't initialize variables in the declaration block. The method initializeProperties is responsible for this task.
selectedFile?: string;
private selectedFileValue?: string;
unsavedFilesValue: { [fileName: string]: string }; // {[fileName]: fileContent}
fileBadges: { [fileName: string]: FileBadge[] };
get selectedFile(): string | undefined {
return this.selectedFileValue;
}

set selectedFile(file: string | undefined) {
this.selectedFileValue = file;
}

/** Code Editor State Variables **/
editorState: EditorState;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,12 @@ <h3 class="card-title justify-content-center">
</div>
<!--Treeview Item Template-->
<ng-template #itemTemplate let-item="item" let-onCollapseExpand="onCollapseExpand">
<!--Problem Statement-->
@if (repositoryFiles?.[item.value] === FileType.PROBLEM_STATEMENT) {
<jhi-code-editor-file-browser-problem-statement [item]="item" (onNodeSelect)="handleNodeSelected(item)" />
}
<!--folder-->
@if (repositoryFiles[item.value] === FileType.FOLDER) {
@if (repositoryFiles?.[item.value] === FileType.FOLDER) {
<jhi-code-editor-file-browser-folder
[item]="item"
[isBeingRenamed]="!!renamingFile && renamingFile![0] === item.value"
Expand All @@ -100,11 +104,11 @@ <h3 class="card-title justify-content-center">
/>
}
<!--file-->
@if (repositoryFiles[item.value] === FileType.FILE) {
@if (repositoryFiles?.[item.value] === FileType.FILE) {
<jhi-code-editor-file-browser-file
[item]="item"
[hasChanges]="repositoryFilesWithInformationAboutChange ? repositoryFilesWithInformationAboutChange[item.value] : false"
[badges]="fileBadges[item.value] || []"
[hasChanges]="repositoryFilesWithInformationAboutChange?.[item.value] ?? false"
[badges]="fileBadges?.[item.value] ?? []"
[isBeingRenamed]="!!renamingFile && renamingFile![0] === item.value"
[hasUnsavedChanges]="unsavedFiles && unsavedFiles.includes(item.value)"
[hasError]="errorFiles && errorFiles.includes(item.value)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
import { TranslateService } from '@ngx-translate/core';
import { TreeViewItem } from 'app/programming/shared/code-editor/treeview/models/tree-view-item';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { CodeEditorFileBrowserProblemStatementComponent } from 'app/programming/manage/code-editor/file-browser/problem-statement/code-editor-file-browser-problem-statement.component';

describe('CodeEditorFileBrowserComponent', () => {
let comp: CodeEditorFileBrowserComponent;
Expand Down Expand Up @@ -47,6 +48,7 @@ describe('CodeEditorFileBrowserComponent', () => {
CodeEditorFileBrowserCreateNodeComponent,
MockComponent(CodeEditorStatusComponent),
TranslatePipeMock,
CodeEditorFileBrowserProblemStatementComponent,
],
providers: [
{ provide: CodeEditorRepositoryService, useClass: MockCodeEditorRepositoryService },
Expand Down Expand Up @@ -74,9 +76,104 @@ describe('CodeEditorFileBrowserComponent', () => {
jest.restoreAllMocks();
});

it('places Problem Statement at the top of the tree', () => {
comp.displayOnly = false;
comp.ngOnInit();

comp.repositoryFiles = {
[comp.PROBLEM_STATEMENT_IDENTIFIER]: FileType.PROBLEM_STATEMENT,
'b.txt': FileType.FILE,
'a.txt': FileType.FILE,
};

comp.setupTreeview();

const values = comp.filesTreeViewItem.map((i) => i.value);
expect(values[0]).toBe(comp.PROBLEM_STATEMENT_IDENTIFIER);
expect(values.slice(1)).toEqual(['a.txt', 'b.txt']);
});

it('adds the Problem Statement entry when not in displayOnly mode', () => {
// ensure fresh state
(comp as any).repositoryFiles = undefined;
comp.displayOnly = false;

comp.ngOnInit();
comp.setupTreeview();

expect(comp.repositoryFiles[comp.PROBLEM_STATEMENT_IDENTIFIER]).toBe(FileType.PROBLEM_STATEMENT);
expect(comp.filesTreeViewItem.map((i) => i.value)).toContain(comp.PROBLEM_STATEMENT_IDENTIFIER);
});

it('removes the Problem Statement entry when displayOnly toggles to true', () => {
// start with PS present
comp.displayOnly = false;
comp.ngOnInit();
(comp as any).handleDisplayOnlyChange?.();

// toggle to repository view (no PS)
comp.displayOnly = true;
(comp as any).handleDisplayOnlyChange?.();

expect(comp.repositoryFiles?.[comp.PROBLEM_STATEMENT_IDENTIFIER]).toBeUndefined();
});

it('should NOT render Problem Statement in repository view (displayOnly=true)', () => {
comp.repositoryFiles = {};
comp.displayOnly = true;
triggerChanges(comp, { property: 'displayOnly', currentValue: true });

comp.repositoryFiles = { 'a.txt': FileType.FILE, folder: FileType.FOLDER };
comp.setupTreeview();
fixture.detectChanges();

expect(Object.keys(comp.repositoryFiles)).not.toContain(comp.PROBLEM_STATEMENT_IDENTIFIER);
expect(comp.filesTreeViewItem.find((i) => i.value === comp.PROBLEM_STATEMENT_IDENTIFIER)).toBeUndefined();

const psNode = debugElement.query(By.css('#file-browser-problem-statement, jhi-code-editor-file-browser-problem-statement'));
expect(psNode).toBeNull();
});

it('should NOT add Problem Statement on error in repository view (displayOnly=true)', () => {
comp.repositoryFiles = {};
comp.displayOnly = true;
triggerChanges(comp, { property: 'displayOnly', currentValue: true });

const status$ = new Subject<any>();
getStatusStub.mockReturnValue(status$);

comp.commitState = CommitState.UNDEFINED;
triggerChanges(comp, { property: 'commitState', currentValue: CommitState.UNDEFINED });
fixture.detectChanges();

status$.error('fatal');
fixture.detectChanges();

expect(comp.filesTreeViewItem).toEqual([]);
const psNode = debugElement.query(By.css('#file-browser-problem-statement, jhi-code-editor-file-browser-problem-statement'));
expect(psNode).toBeNull();
});

it('should render Problem Statement when not in repository view (displayOnly=false)', () => {
comp.displayOnly = false;
triggerChanges(comp, { property: 'displayOnly', currentValue: false });

comp.repositoryFiles = {};
// even with empty repo ensure PS is added, then build tree
(comp as any).initializeRepositoryFiles();
comp.setupTreeview();
fixture.detectChanges();

expect(Object.keys(comp.repositoryFiles)).toContain(comp.PROBLEM_STATEMENT_IDENTIFIER);
const psItem = comp.filesTreeViewItem.find((i) => i.value === comp.PROBLEM_STATEMENT_IDENTIFIER);
expect(psItem).toBeDefined();

const psNode = debugElement.query(By.css('#file-browser-problem-statement, jhi-code-editor-file-browser-problem-statement'));
expect(psNode).not.toBeNull();
});

it('should create no treeviewItems if getRepositoryContent returns an empty result', () => {
const repositoryContent: { [fileName: string]: string } = {};
const expectedFileTreeItems: TreeViewItem<string>[] = [];
getRepositoryContentStub.mockReturnValue(of(repositoryContent));
getStatusStub.mockReturnValue(of({ repositoryStatus: CommitState.CLEAN }));
comp.commitState = CommitState.UNDEFINED;
Expand All @@ -85,8 +182,16 @@ describe('CodeEditorFileBrowserComponent', () => {
fixture.detectChanges();

expect(comp.isLoadingFiles).toBeFalse();
expect(comp.repositoryFiles).toEqual(repositoryContent);
expect(comp.filesTreeViewItem).toEqual(expectedFileTreeItems);
// repositoryFiles now contains only PS
expect(comp.repositoryFiles).toEqual({
[comp.PROBLEM_STATEMENT_IDENTIFIER]: FileType.PROBLEM_STATEMENT,
});

// tree now has exactly 1 item: PS
expect(comp.filesTreeViewItem).toHaveLength(1);
expect(comp.filesTreeViewItem[0].value).toBe(comp.PROBLEM_STATEMENT_IDENTIFIER);

// still no regular folders/files rendered
const renderedFolders = debugElement.queryAll(By.css('jhi-code-editor-file-browser-folder'));
const renderedFiles = debugElement.queryAll(By.css('jhi-code-editor-file-browser-file'));
expect(renderedFolders).toHaveLength(0);
Expand All @@ -102,7 +207,10 @@ describe('CodeEditorFileBrowserComponent', () => {
triggerChanges(comp, { property: 'commitState', currentValue: CommitState.UNDEFINED });
fixture.detectChanges();
expect(comp.isLoadingFiles).toBeFalse();
expect(comp.repositoryFiles).toEqual(repositoryContent);
expect(comp.repositoryFiles).toEqual({
...repositoryContent,
[comp.PROBLEM_STATEMENT_IDENTIFIER]: FileType.PROBLEM_STATEMENT,
});
const renderedFolders = debugElement.queryAll(By.css('jhi-code-editor-file-browser-folder'));
const renderedFiles = debugElement.queryAll(By.css('jhi-code-editor-file-browser-file'));
expect(renderedFolders).toHaveLength(1);
Expand Down Expand Up @@ -233,23 +341,24 @@ describe('CodeEditorFileBrowserComponent', () => {
...allowedFiles,
...forbiddenFiles,
};
const expectedFileTreeItems = [
new TreeViewItem({
internalDisabled: false,
internalChecked: false,
internalCollapsed: false,
text: 'file1',
value: 'file1',
} as any),
].map((x) => x.toString());
getRepositoryContentStub.mockReturnValue(of(repositoryContent));
getStatusStub.mockReturnValue(of({ repositoryStatus: CommitState.CLEAN }));
comp.commitState = CommitState.UNDEFINED;
triggerChanges(comp, { property: 'commitState', currentValue: CommitState.UNDEFINED });
fixture.detectChanges();
expect(comp.isLoadingFiles).toBeFalse();
expect(comp.repositoryFiles).toEqual(allowedFiles);
expect(comp.filesTreeViewItem.map((x) => x.toString())).toEqual(expectedFileTreeItems);
expect(comp.repositoryFiles).toEqual({
...allowedFiles,
[comp.PROBLEM_STATEMENT_IDENTIFIER]: FileType.PROBLEM_STATEMENT,
});

// tree should contain exactly the allowed file and PS
expect(comp.filesTreeViewItem).toHaveLength(2);
const values = comp.filesTreeViewItem.map((i) => i.value);
expect(values).toContain('allowedFile.java');
expect(values).toContain(comp.PROBLEM_STATEMENT_IDENTIFIER);

// rendered components: one file, zero folders
const renderedFolders = debugElement.queryAll(By.css('jhi-code-editor-file-browser-folder'));
const renderedFiles = debugElement.queryAll(By.css('jhi-code-editor-file-browser-file'));
expect(renderedFolders).toHaveLength(0);
Expand Down Expand Up @@ -295,10 +404,20 @@ describe('CodeEditorFileBrowserComponent', () => {
fixture.detectChanges();
expect(comp.commitState).toEqual(CommitState.COULD_NOT_BE_RETRIEVED);
expect(comp.isLoadingFiles).toBeFalse();
expect(comp.repositoryFiles).toBeUndefined();
expect(comp.filesTreeViewItem).toBeUndefined();

// PS is still present
expect(comp.repositoryFiles).toEqual({
[comp.PROBLEM_STATEMENT_IDENTIFIER]: FileType.PROBLEM_STATEMENT,
});

// tree was built to show PS
expect(comp.filesTreeViewItem).toHaveLength(1);
expect(comp.filesTreeViewItem[0].value).toBe(comp.PROBLEM_STATEMENT_IDENTIFIER);

expect(onErrorSpy).toHaveBeenCalledOnce();
expect(loadFilesSpy).not.toHaveBeenCalled();

// still no regular folders/files rendered
const renderedFolders = debugElement.queryAll(By.css('jhi-code-editor-file-browser-folder'));
const renderedFiles = debugElement.queryAll(By.css('jhi-code-editor-file-browser-file'));
expect(renderedFolders).toHaveLength(0);
Expand All @@ -321,9 +440,14 @@ describe('CodeEditorFileBrowserComponent', () => {

fixture.detectChanges();
expect(comp.isLoadingFiles).toBeFalse();
expect(comp.repositoryFiles).toBeUndefined();
expect(comp.filesTreeViewItem).toBeUndefined();
expect(comp.repositoryFiles).toEqual({
[comp.PROBLEM_STATEMENT_IDENTIFIER]: FileType.PROBLEM_STATEMENT,
});
expect(comp.filesTreeViewItem).toHaveLength(1);
expect(comp.filesTreeViewItem[0].value).toBe(comp.PROBLEM_STATEMENT_IDENTIFIER);

expect(onErrorSpy).toHaveBeenCalledOnce();

const renderedFolders = debugElement.queryAll(By.css('jhi-code-editor-file-browser-folder'));
const renderedFiles = debugElement.queryAll(By.css('jhi-code-editor-file-browser-file'));
expect(renderedFolders).toHaveLength(0);
Expand Down Expand Up @@ -628,8 +752,11 @@ describe('CodeEditorFileBrowserComponent', () => {
expect(renameFileStub).toHaveBeenCalledWith(fileName, afterRename);
expect(comp.renamingFile).toBeUndefined();
expect(onFileChangeSpy).toHaveBeenCalledOnce();
expect(comp.repositoryFiles).toEqual({ folder2: FileType.FOLDER, [afterRename]: FileType.FILE });

expect(comp.repositoryFiles).toEqual({
folder2: FileType.FOLDER,
[afterRename]: FileType.FILE,
[comp.PROBLEM_STATEMENT_IDENTIFIER]: FileType.PROBLEM_STATEMENT,
});
filesInTreeHtml = debugElement.queryAll(By.css('jhi-code-editor-file-browser-file'));
expect(filesInTreeHtml).toHaveLength(1);
foldersInTreeHtml = debugElement.queryAll(By.css('jhi-code-editor-file-browser-folder'));
Expand Down Expand Up @@ -706,6 +833,7 @@ describe('CodeEditorFileBrowserComponent', () => {
[[afterRename, 'file2'].join('/')]: FileType.FILE,
[afterRename]: FileType.FOLDER,
folder2: FileType.FOLDER,
[comp.PROBLEM_STATEMENT_IDENTIFIER]: FileType.PROBLEM_STATEMENT,
});

filesInTreeHtml = debugElement.queryAll(By.css('jhi-code-editor-file-browser-file'));
Expand Down
Loading
Loading