Skip to content

Commit 8774af7

Browse files
tomzohartomzo42
andauthored
feat: filesystem navigator (#93)
Co-authored-by: tomzo42 <tom.zohar@hibob.io> Co-authored-by: Tom Zohar <60091317+Tcodrz@users.noreply.github.com>
1 parent a2ea9b6 commit 8774af7

13 files changed

+345
-1
lines changed
Lines changed: 16 additions & 0 deletions
Loading
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
export interface Directory {
22
name: string;
3-
isNG?: boolean;
3+
isNG: boolean;
44
isFavorite?: boolean;
55
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import {
2+
HttpClientTestingModule,
3+
HttpTestingController,
4+
} from '@angular/common/http/testing';
5+
import { TestBed } from '@angular/core/testing';
6+
7+
import { WorkspaceManagerApiService } from './workspace-manager-api.service';
8+
9+
describe('WorkspaceManagerApiService', () => {
10+
let service: WorkspaceManagerApiService;
11+
let http: HttpTestingController;
12+
13+
beforeEach(() => {
14+
TestBed.configureTestingModule({
15+
imports: [HttpClientTestingModule],
16+
});
17+
18+
http = TestBed.inject(HttpTestingController);
19+
service = TestBed.inject(WorkspaceManagerApiService);
20+
});
21+
22+
it('should be created', () => {
23+
expect(service).toBeTruthy();
24+
});
25+
26+
describe('getHomeDir', () => {
27+
it('should call the get homedir api', () => {
28+
service.getHomeDir().subscribe();
29+
const request = http.expectOne('/api/workspace-manager/homedir');
30+
expect(request.request.method).toEqual('GET');
31+
});
32+
});
33+
34+
describe('getDirectoriesInPath', () => {
35+
it('should call get directories in path api with path as params', () => {
36+
service.getDirectoriesInPath('/mock/path').subscribe();
37+
const req = http.expectOne('/api/workspace-manager/dir?path=/mock/path');
38+
expect(req.request.method).toEqual('GET');
39+
});
40+
});
41+
42+
describe('getPathSeparator', () => {
43+
it('should call the get path separator api', () => {
44+
service.getPathSeparator().subscribe();
45+
const req = http.expectOne('/api/workspace-manager/path-sep');
46+
expect(req.request.method).toEqual('GET');
47+
});
48+
});
49+
});
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { HttpClient } from '@angular/common/http';
2+
import { inject, Injectable } from '@angular/core';
3+
import { Directory } from '@angular-cli-gui/shared/data';
4+
import { Observable } from 'rxjs';
5+
6+
@Injectable({
7+
providedIn: 'root',
8+
})
9+
export class WorkspaceManagerApiService {
10+
private readonly http = inject(HttpClient);
11+
private readonly API_URL = '/api/workspace-manager';
12+
13+
getHomeDir(): Observable<string> {
14+
return this.http.get(`${this.API_URL}/homedir`, {
15+
responseType: 'text',
16+
});
17+
}
18+
19+
getDirectoriesInPath(path: string): Observable<Directory[]> {
20+
return this.http.get<Directory[]>(`${this.API_URL}/dir`, {
21+
params: { path },
22+
});
23+
}
24+
25+
getPathSeparator(): Observable<string> {
26+
return this.http.get(`${this.API_URL}/path-sep`, { responseType: 'text' });
27+
}
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<div class="path">
2+
<div *ngFor="let part of pathParts" class="path-part">
3+
<button
4+
*ngIf="part"
5+
mat-button
6+
class="path-part-button"
7+
(click)="onPartClicked(part)"
8+
>
9+
{{ part }}
10+
</button>
11+
<div>{{ separator }}</div>
12+
</div>
13+
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
.path {
2+
display: flex;
3+
}
4+
5+
.path-part {
6+
display: flex;
7+
align-items: center;
8+
}
9+
10+
.path-part-button {
11+
margin-left: 2px;
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
3+
import { FilesystemNavigatorToolbarComponent } from './filesystem-navigator-toolbar.component';
4+
5+
describe('FilesystemNavigatorToolbarComponent', () => {
6+
let component: FilesystemNavigatorToolbarComponent;
7+
let fixture: ComponentFixture<FilesystemNavigatorToolbarComponent>;
8+
9+
beforeEach(async () => {
10+
await TestBed.configureTestingModule({
11+
imports: [FilesystemNavigatorToolbarComponent],
12+
}).compileComponents();
13+
14+
fixture = TestBed.createComponent(FilesystemNavigatorToolbarComponent);
15+
component = fixture.componentInstance;
16+
fixture.componentRef.setInput('path', '/mock/path');
17+
fixture.componentRef.setInput('separator', '/');
18+
fixture.detectChanges();
19+
});
20+
21+
it('should create', () => {
22+
expect(component).toBeTruthy();
23+
});
24+
25+
describe('ngOnChanges', () => {
26+
it('should split the path input and save the parts', () => {
27+
expect(component.pathParts).toEqual(['mock', 'path']);
28+
});
29+
});
30+
31+
describe('onPathClicked', () => {
32+
it('should emit path changed event with the new path', () => {
33+
const pathChangeEventSpy = jest.spyOn(component.pathChange, 'emit');
34+
component.onPartClicked('mock');
35+
expect(pathChangeEventSpy).toHaveBeenCalledWith('/mock');
36+
});
37+
});
38+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { NgForOf, NgIf } from '@angular/common';
2+
import {
3+
ChangeDetectionStrategy,
4+
Component,
5+
EventEmitter,
6+
Input,
7+
OnChanges,
8+
Output,
9+
} from '@angular/core';
10+
import { MatButtonModule } from '@angular/material/button';
11+
12+
@Component({
13+
selector: 'cli-filesystem-navigator-toolbar',
14+
standalone: true,
15+
imports: [NgForOf, NgIf, MatButtonModule],
16+
templateUrl: './filesystem-navigator-toolbar.component.html',
17+
styleUrls: ['./filesystem-navigator-toolbar.component.scss'],
18+
changeDetection: ChangeDetectionStrategy.OnPush,
19+
})
20+
export class FilesystemNavigatorToolbarComponent implements OnChanges {
21+
@Input() path: string | null = null;
22+
@Input() separator: string | null = '';
23+
@Output() pathChange = new EventEmitter<string>();
24+
25+
pathParts: string[] = [];
26+
27+
ngOnChanges(): void {
28+
this.pathParts = (this.path as string)
29+
.split(this.separator as string)
30+
.filter((v) => !!v);
31+
}
32+
33+
onPartClicked(pathPart: string): void {
34+
const index = this.pathParts.indexOf(pathPart);
35+
const newPath = this.pathParts
36+
.slice(0, index + 1)
37+
.join(this.separator as string);
38+
this.pathChange.emit(`${this.separator}${newPath}`);
39+
}
40+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<cli-filesystem-navigator-toolbar
2+
[path]="path"
3+
[separator]="separator"
4+
(pathChange)="onPathChanged($event)"
5+
></cli-filesystem-navigator-toolbar>
6+
7+
<div class="list-wrapper">
8+
<mat-list>
9+
<ng-container *ngFor="let dir of directories">
10+
<mat-list-item (click)="onDirClicked(dir)">
11+
<div matListItemIcon>
12+
<ng-container
13+
[ngTemplateOutlet]="dir.isNG ? angularIcon : folderIcon"
14+
></ng-container>
15+
</div>
16+
17+
<div matListItemTitle>
18+
{{ dir.name }}
19+
</div>
20+
</mat-list-item>
21+
22+
<mat-divider></mat-divider>
23+
</ng-container>
24+
</mat-list>
25+
</div>
26+
27+
<ng-template #folderIcon>
28+
<mat-icon>folder</mat-icon>
29+
</ng-template>
30+
31+
<ng-template #angularIcon>
32+
<img class="angular-logo" src="/assets/angular.svg" />
33+
</ng-template>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
:host {
2+
.list-wrapper {
3+
overflow: auto;
4+
margin-top: 16px;
5+
height: inherit;
6+
max-height: 100%;
7+
}
8+
9+
mat-list-item:hover {
10+
cursor: pointer;
11+
}
12+
13+
.angular-logo {
14+
width: 25px;
15+
height: 25px;
16+
}
17+
}

0 commit comments

Comments
 (0)