diff --git a/apps/cli-gui/src/assets/angular.svg b/apps/cli-gui/src/assets/angular.svg
new file mode 100644
index 0000000..bf081ac
--- /dev/null
+++ b/apps/cli-gui/src/assets/angular.svg
@@ -0,0 +1,16 @@
+
+
+
diff --git a/libs/shared/data/src/lib/directory.interface.ts b/libs/shared/data/src/lib/directory.interface.ts
index 9ba312a..79ba25c 100644
--- a/libs/shared/data/src/lib/directory.interface.ts
+++ b/libs/shared/data/src/lib/directory.interface.ts
@@ -1,5 +1,5 @@
export interface Directory {
name: string;
- isNG?: boolean;
+ isNG: boolean;
isFavorite?: boolean;
}
diff --git a/libs/workspace-manager/src/lib/data-access/workspace-manager-api.service.spec.ts b/libs/workspace-manager/src/lib/data-access/workspace-manager-api.service.spec.ts
new file mode 100644
index 0000000..eb4d3b2
--- /dev/null
+++ b/libs/workspace-manager/src/lib/data-access/workspace-manager-api.service.spec.ts
@@ -0,0 +1,49 @@
+import {
+ HttpClientTestingModule,
+ HttpTestingController,
+} from '@angular/common/http/testing';
+import { TestBed } from '@angular/core/testing';
+
+import { WorkspaceManagerApiService } from './workspace-manager-api.service';
+
+describe('WorkspaceManagerApiService', () => {
+ let service: WorkspaceManagerApiService;
+ let http: HttpTestingController;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [HttpClientTestingModule],
+ });
+
+ http = TestBed.inject(HttpTestingController);
+ service = TestBed.inject(WorkspaceManagerApiService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ describe('getHomeDir', () => {
+ it('should call the get homedir api', () => {
+ service.getHomeDir().subscribe();
+ const request = http.expectOne('/api/workspace-manager/homedir');
+ expect(request.request.method).toEqual('GET');
+ });
+ });
+
+ describe('getDirectoriesInPath', () => {
+ it('should call get directories in path api with path as params', () => {
+ service.getDirectoriesInPath('/mock/path').subscribe();
+ const req = http.expectOne('/api/workspace-manager/dir?path=/mock/path');
+ expect(req.request.method).toEqual('GET');
+ });
+ });
+
+ describe('getPathSeparator', () => {
+ it('should call the get path separator api', () => {
+ service.getPathSeparator().subscribe();
+ const req = http.expectOne('/api/workspace-manager/path-sep');
+ expect(req.request.method).toEqual('GET');
+ });
+ });
+});
diff --git a/libs/workspace-manager/src/lib/data-access/workspace-manager-api.service.ts b/libs/workspace-manager/src/lib/data-access/workspace-manager-api.service.ts
new file mode 100644
index 0000000..3cf6d12
--- /dev/null
+++ b/libs/workspace-manager/src/lib/data-access/workspace-manager-api.service.ts
@@ -0,0 +1,28 @@
+import { HttpClient } from '@angular/common/http';
+import { inject, Injectable } from '@angular/core';
+import { Directory } from '@angular-cli-gui/shared/data';
+import { Observable } from 'rxjs';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class WorkspaceManagerApiService {
+ private readonly http = inject(HttpClient);
+ private readonly API_URL = '/api/workspace-manager';
+
+ getHomeDir(): Observable {
+ return this.http.get(`${this.API_URL}/homedir`, {
+ responseType: 'text',
+ });
+ }
+
+ getDirectoriesInPath(path: string): Observable {
+ return this.http.get(`${this.API_URL}/dir`, {
+ params: { path },
+ });
+ }
+
+ getPathSeparator(): Observable {
+ return this.http.get(`${this.API_URL}/path-sep`, { responseType: 'text' });
+ }
+}
diff --git a/libs/workspace-manager/src/lib/ui/filesystem-navigator/filesystem-navigator-toolbar/filesystem-navigator-toolbar.component.html b/libs/workspace-manager/src/lib/ui/filesystem-navigator/filesystem-navigator-toolbar/filesystem-navigator-toolbar.component.html
new file mode 100644
index 0000000..cb34111
--- /dev/null
+++ b/libs/workspace-manager/src/lib/ui/filesystem-navigator/filesystem-navigator-toolbar/filesystem-navigator-toolbar.component.html
@@ -0,0 +1,13 @@
+
+
+
+
{{ separator }}
+
+
diff --git a/libs/workspace-manager/src/lib/ui/filesystem-navigator/filesystem-navigator-toolbar/filesystem-navigator-toolbar.component.scss b/libs/workspace-manager/src/lib/ui/filesystem-navigator/filesystem-navigator-toolbar/filesystem-navigator-toolbar.component.scss
new file mode 100644
index 0000000..27ecc9c
--- /dev/null
+++ b/libs/workspace-manager/src/lib/ui/filesystem-navigator/filesystem-navigator-toolbar/filesystem-navigator-toolbar.component.scss
@@ -0,0 +1,12 @@
+.path {
+ display: flex;
+}
+
+.path-part {
+ display: flex;
+ align-items: center;
+}
+
+.path-part-button {
+ margin-left: 2px;
+}
diff --git a/libs/workspace-manager/src/lib/ui/filesystem-navigator/filesystem-navigator-toolbar/filesystem-navigator-toolbar.component.spec.ts b/libs/workspace-manager/src/lib/ui/filesystem-navigator/filesystem-navigator-toolbar/filesystem-navigator-toolbar.component.spec.ts
new file mode 100644
index 0000000..a5d49aa
--- /dev/null
+++ b/libs/workspace-manager/src/lib/ui/filesystem-navigator/filesystem-navigator-toolbar/filesystem-navigator-toolbar.component.spec.ts
@@ -0,0 +1,38 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { FilesystemNavigatorToolbarComponent } from './filesystem-navigator-toolbar.component';
+
+describe('FilesystemNavigatorToolbarComponent', () => {
+ let component: FilesystemNavigatorToolbarComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [FilesystemNavigatorToolbarComponent],
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(FilesystemNavigatorToolbarComponent);
+ component = fixture.componentInstance;
+ fixture.componentRef.setInput('path', '/mock/path');
+ fixture.componentRef.setInput('separator', '/');
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ describe('ngOnChanges', () => {
+ it('should split the path input and save the parts', () => {
+ expect(component.pathParts).toEqual(['mock', 'path']);
+ });
+ });
+
+ describe('onPathClicked', () => {
+ it('should emit path changed event with the new path', () => {
+ const pathChangeEventSpy = jest.spyOn(component.pathChange, 'emit');
+ component.onPartClicked('mock');
+ expect(pathChangeEventSpy).toHaveBeenCalledWith('/mock');
+ });
+ });
+});
diff --git a/libs/workspace-manager/src/lib/ui/filesystem-navigator/filesystem-navigator-toolbar/filesystem-navigator-toolbar.component.ts b/libs/workspace-manager/src/lib/ui/filesystem-navigator/filesystem-navigator-toolbar/filesystem-navigator-toolbar.component.ts
new file mode 100644
index 0000000..4800abd
--- /dev/null
+++ b/libs/workspace-manager/src/lib/ui/filesystem-navigator/filesystem-navigator-toolbar/filesystem-navigator-toolbar.component.ts
@@ -0,0 +1,40 @@
+import { NgForOf, NgIf } from '@angular/common';
+import {
+ ChangeDetectionStrategy,
+ Component,
+ EventEmitter,
+ Input,
+ OnChanges,
+ Output,
+} from '@angular/core';
+import { MatButtonModule } from '@angular/material/button';
+
+@Component({
+ selector: 'cli-filesystem-navigator-toolbar',
+ standalone: true,
+ imports: [NgForOf, NgIf, MatButtonModule],
+ templateUrl: './filesystem-navigator-toolbar.component.html',
+ styleUrls: ['./filesystem-navigator-toolbar.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class FilesystemNavigatorToolbarComponent implements OnChanges {
+ @Input() path: string | null = null;
+ @Input() separator: string | null = '';
+ @Output() pathChange = new EventEmitter();
+
+ pathParts: string[] = [];
+
+ ngOnChanges(): void {
+ this.pathParts = (this.path as string)
+ .split(this.separator as string)
+ .filter((v) => !!v);
+ }
+
+ onPartClicked(pathPart: string): void {
+ const index = this.pathParts.indexOf(pathPart);
+ const newPath = this.pathParts
+ .slice(0, index + 1)
+ .join(this.separator as string);
+ this.pathChange.emit(`${this.separator}${newPath}`);
+ }
+}
diff --git a/libs/workspace-manager/src/lib/ui/filesystem-navigator/filesystem-navigator.component.html b/libs/workspace-manager/src/lib/ui/filesystem-navigator/filesystem-navigator.component.html
new file mode 100644
index 0000000..65984b1
--- /dev/null
+++ b/libs/workspace-manager/src/lib/ui/filesystem-navigator/filesystem-navigator.component.html
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ dir.name }}
+
+
+
+
+
+
+
+
+
+ folder
+
+
+
+
+
diff --git a/libs/workspace-manager/src/lib/ui/filesystem-navigator/filesystem-navigator.component.scss b/libs/workspace-manager/src/lib/ui/filesystem-navigator/filesystem-navigator.component.scss
new file mode 100644
index 0000000..0e6b4fe
--- /dev/null
+++ b/libs/workspace-manager/src/lib/ui/filesystem-navigator/filesystem-navigator.component.scss
@@ -0,0 +1,17 @@
+:host {
+ .list-wrapper {
+ overflow: auto;
+ margin-top: 16px;
+ height: inherit;
+ max-height: 100%;
+ }
+
+ mat-list-item:hover {
+ cursor: pointer;
+ }
+
+ .angular-logo {
+ width: 25px;
+ height: 25px;
+ }
+}
diff --git a/libs/workspace-manager/src/lib/ui/filesystem-navigator/filesystem-navigator.component.spec.ts b/libs/workspace-manager/src/lib/ui/filesystem-navigator/filesystem-navigator.component.spec.ts
new file mode 100644
index 0000000..93a8c72
--- /dev/null
+++ b/libs/workspace-manager/src/lib/ui/filesystem-navigator/filesystem-navigator.component.spec.ts
@@ -0,0 +1,44 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { Directory } from '@angular-cli-gui/shared/data';
+
+import { FilesystemNavigatorComponent } from './filesystem-navigator.component';
+
+describe('FilesystemNavigatorComponent', () => {
+ let component: FilesystemNavigatorComponent;
+ let fixture: ComponentFixture;
+
+ const directoriesMock: Directory[] = [];
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [FilesystemNavigatorComponent],
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(FilesystemNavigatorComponent);
+ component = fixture.componentInstance;
+ component.path = '/mock/path';
+ component.directories = directoriesMock;
+ component.separator = '/';
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ describe('onPathChanged', () => {
+ it('should emit path changed event', () => {
+ const pathChangeEmitSpy = jest.spyOn(component.pathChange, 'emit');
+ component.onPathChanged('new/path');
+ expect(pathChangeEmitSpy).toHaveBeenCalledWith('new/path');
+ });
+ });
+
+ describe('onDirClicked', () => {
+ it('should append the dir name to the current path and emit path changed event', () => {
+ const pathChangeEmitSpy = jest.spyOn(component.pathChange, 'emit');
+ component.onDirClicked({ name: 'abc', isNG: false });
+ expect(pathChangeEmitSpy).toHaveBeenCalledWith('/mock/path/abc');
+ });
+ });
+});
diff --git a/libs/workspace-manager/src/lib/ui/filesystem-navigator/filesystem-navigator.component.ts b/libs/workspace-manager/src/lib/ui/filesystem-navigator/filesystem-navigator.component.ts
new file mode 100644
index 0000000..3373eb3
--- /dev/null
+++ b/libs/workspace-manager/src/lib/ui/filesystem-navigator/filesystem-navigator.component.ts
@@ -0,0 +1,52 @@
+import { AsyncPipe, NgForOf, NgIf, NgTemplateOutlet } from '@angular/common';
+import {
+ ChangeDetectionStrategy,
+ Component,
+ EventEmitter,
+ Input,
+ Output,
+} from '@angular/core';
+import { MatDividerModule } from '@angular/material/divider';
+import { MatIconModule } from '@angular/material/icon';
+import { MatListModule } from '@angular/material/list';
+import { Directory } from '@angular-cli-gui/shared/data';
+
+import { FilesystemNavigatorToolbarComponent } from './filesystem-navigator-toolbar/filesystem-navigator-toolbar.component';
+
+@Component({
+ selector: 'cli-filesystem-navigator',
+ standalone: true,
+ imports: [
+ MatListModule,
+ NgForOf,
+ NgIf,
+ MatDividerModule,
+ MatIconModule,
+ FilesystemNavigatorToolbarComponent,
+ AsyncPipe,
+ NgTemplateOutlet,
+ ],
+ templateUrl: './filesystem-navigator.component.html',
+ styleUrls: ['./filesystem-navigator.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class FilesystemNavigatorComponent {
+ @Input() path: string | null = null;
+ @Input() directories: Directory[] | null = [];
+ @Input() separator: string | null = '';
+
+ @Output() pathChange = new EventEmitter();
+
+ onPathChanged(path: string): void {
+ this.emitPathChangeEvent(path);
+ }
+
+ onDirClicked(directory: Directory): void {
+ const path = `${this.path}${this.separator}${directory.name}`;
+ this.onPathChanged(path);
+ }
+
+ private emitPathChangeEvent(path: string): void {
+ this.pathChange.emit(path);
+ }
+}
diff --git a/libs/workspace-manager/src/lib/ui/index.ts b/libs/workspace-manager/src/lib/ui/index.ts
new file mode 100644
index 0000000..9c6f94f
--- /dev/null
+++ b/libs/workspace-manager/src/lib/ui/index.ts
@@ -0,0 +1,2 @@
+export * from './filesystem-navigator/filesystem-navigator-toolbar/filesystem-navigator-toolbar.component';
+export * from './filesystem-navigator/filesystem-navigator.component';