diff --git a/libs/workspace-manager/src/lib/features/connect-workspace/connect-workspace.component.html b/libs/workspace-manager/src/lib/features/connect-workspace/connect-workspace.component.html index 04265ae..574e9da 100644 --- a/libs/workspace-manager/src/lib/features/connect-workspace/connect-workspace.component.html +++ b/libs/workspace-manager/src/lib/features/connect-workspace/connect-workspace.component.html @@ -1,12 +1,24 @@ -
-

Connect your Angular workspace

- - The absolute path to your workspace - - - -
+
+ + + +
+ + + + diff --git a/libs/workspace-manager/src/lib/features/connect-workspace/connect-workspace.component.scss b/libs/workspace-manager/src/lib/features/connect-workspace/connect-workspace.component.scss index f0912f6..e3e1359 100644 --- a/libs/workspace-manager/src/lib/features/connect-workspace/connect-workspace.component.scss +++ b/libs/workspace-manager/src/lib/features/connect-workspace/connect-workspace.component.scss @@ -1,14 +1,11 @@ :host { display: flex; - flex: 1; -} - -.connect-workspace-form { - display: grid; - place-content: center; - width: 100%; + flex-direction: column; + align-items: center; - .workspace-path-field { - min-width: 350px; + .fs-navigator-wrapper { + width: 70vw; + height: 70vh; + margin-top: 24px; } } diff --git a/libs/workspace-manager/src/lib/features/connect-workspace/connect-workspace.component.spec.ts b/libs/workspace-manager/src/lib/features/connect-workspace/connect-workspace.component.spec.ts index fc5d12b..f5ac1fc 100644 --- a/libs/workspace-manager/src/lib/features/connect-workspace/connect-workspace.component.spec.ts +++ b/libs/workspace-manager/src/lib/features/connect-workspace/connect-workspace.component.spec.ts @@ -2,14 +2,33 @@ import { HttpClientTestingModule } from '@angular/common/http/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { RouterTestingModule } from '@angular/router/testing'; +import { CURRENT_WORKSPACE_PATH } from '@angular-cli-gui/shared/data'; +import { ConnectWorkspaceService } from '@angular-cli-gui/workspace-manager'; + +import { WorkspaceManagerApiService } from '../../data-access/workspace-manager-api.service'; +import { + ConnectWorkspaceServiceMock, + DIRECTORIES_MOCK, + getConnectWorkspaceServiceMock, + getWorkspaceManagerApiServiceMock, + HOMEDIR_PATH_MOCK, + PATH_SEPARATOR_MOCK, + WorkspaceManagerApiServiceMock, +} from '../../mocks/connect-workspace.mock'; import { ConnectWorkspaceComponent } from './connect-workspace.component'; describe('ConnectWorkspaceComponent', () => { let component: ConnectWorkspaceComponent; let fixture: ComponentFixture; + let workspaceManagerApiServiceMock: WorkspaceManagerApiServiceMock; + let connectWorkspaceServiceMock: ConnectWorkspaceServiceMock; beforeEach(async () => { + workspaceManagerApiServiceMock = getWorkspaceManagerApiServiceMock(); + + connectWorkspaceServiceMock = getConnectWorkspaceServiceMock(); + await TestBed.configureTestingModule({ imports: [ ConnectWorkspaceComponent, @@ -17,14 +36,104 @@ describe('ConnectWorkspaceComponent', () => { RouterTestingModule, NoopAnimationsModule, ], + providers: [ + { + provide: WorkspaceManagerApiService, + useValue: workspaceManagerApiServiceMock, + }, + { + provide: ConnectWorkspaceService, + useValue: connectWorkspaceServiceMock, + }, + ], }).compileComponents(); fixture = TestBed.createComponent(ConnectWorkspaceComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); it('should create', () => { + fixture.detectChanges(); expect(component).toBeTruthy(); }); + + it('should call workspaceManagerApiService.getPathSeparator', () => { + fixture.detectChanges(); + expect(workspaceManagerApiServiceMock.getPathSeparator).toHaveBeenCalled(); + }); + + it('should initialize pathSeparator subject', () => { + fixture.detectChanges(); + expect(component.pathSeparator$.getValue()).toEqual(PATH_SEPARATOR_MOCK); + }); + + it('should call workspaceManagerApiService.getHomeDir when no workspace path in session storage', () => { + fixture.detectChanges(); + expect(workspaceManagerApiServiceMock.getHomeDir).toHaveBeenCalled(); + }); + + it('should not call workspaceManagerApiService.getHomeDir when session storage has saved workspace path', () => { + sessionStorage.setItem(CURRENT_WORKSPACE_PATH, 'workspace/path'); + fixture.detectChanges(); + expect(workspaceManagerApiServiceMock.getHomeDir).not.toHaveBeenCalled(); + sessionStorage.clear(); + }); + + it('should initialize path subject with home dir path', () => { + fixture.detectChanges(); + expect(component.path$.getValue()).toEqual(HOMEDIR_PATH_MOCK); + }); + + it('should set isAngularWorkspace subject with false when path is not a valid workspace path', () => { + fixture.detectChanges(); + expect(component.isAngularWorkspace$.getValue()).toBe(false); + }); + + it('should set isAngularWorkspace subject with true when path is Angular workspace', () => { + fixture.detectChanges(); + const angularWorkspaceDirName = DIRECTORIES_MOCK[1].name; + component.path$.next( + `${HOMEDIR_PATH_MOCK}${PATH_SEPARATOR_MOCK}user${PATH_SEPARATOR_MOCK}${angularWorkspaceDirName}` + ); + expect(component.isAngularWorkspace$.getValue()).toBe(true); + }); + + it('should call workspaceManagerApiService.getDirectoriesInPath with path from path$ subject', () => { + fixture.detectChanges(); + expect( + workspaceManagerApiServiceMock.getDirectoriesInPath + ).toHaveBeenCalledWith(HOMEDIR_PATH_MOCK); + }); + + it('should initialize the directories subject', () => { + fixture.detectChanges(); + expect(component.directories$.getValue()).toEqual(DIRECTORIES_MOCK); + }); + + it('should call connectService.connectWorkspace with path', () => { + fixture.detectChanges(); + component.connectWorkspace(); + expect(connectWorkspaceServiceMock.connectWorkspace).toHaveBeenCalledWith( + HOMEDIR_PATH_MOCK + ); + }); + + describe('onPathChanged', () => { + beforeEach(() => { + fixture.detectChanges(); + }); + + it('should not call path$ subject next when new path equals current path', () => { + const pathNextSpy = jest.spyOn(component.path$, 'next'); + component.onPathChanged(HOMEDIR_PATH_MOCK); + expect(pathNextSpy).not.toHaveBeenCalled(); + }); + + it('should call path$ subject next with new path when new path is different than current path', () => { + const newPathMock = '/new/path/1'; + const pathNextSpy = jest.spyOn(component.path$, 'next'); + component.onPathChanged(newPathMock); + expect(pathNextSpy).toHaveBeenCalledWith(newPathMock); + }); + }); }); diff --git a/libs/workspace-manager/src/lib/features/connect-workspace/connect-workspace.component.ts b/libs/workspace-manager/src/lib/features/connect-workspace/connect-workspace.component.ts index 6cbe049..3cfa883 100644 --- a/libs/workspace-manager/src/lib/features/connect-workspace/connect-workspace.component.ts +++ b/libs/workspace-manager/src/lib/features/connect-workspace/connect-workspace.component.ts @@ -1,36 +1,109 @@ import { CommonModule } from '@angular/common'; -import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; import { - FormControl, - FormGroup, - ReactiveFormsModule, - Validators, -} from '@angular/forms'; + ChangeDetectionStrategy, + Component, + inject, + OnDestroy, + OnInit, +} from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; -import { MatInputModule } from '@angular/material/input'; +import { + CURRENT_WORKSPACE_PATH, + Directory, +} from '@angular-cli-gui/shared/data'; +import { + BehaviorSubject, + combineLatest, + of, + Subject, + switchMap, + takeUntil, +} from 'rxjs'; import { ConnectWorkspaceService } from '../../data-access/connect-workspace.service'; +import { WorkspaceManagerApiService } from '../../data-access/workspace-manager-api.service'; +import { FilesystemNavigatorComponent } from '../../ui'; @Component({ selector: 'cli-connect-workspace', standalone: true, - imports: [CommonModule, MatInputModule, MatButtonModule, ReactiveFormsModule], + imports: [CommonModule, MatButtonModule, FilesystemNavigatorComponent], templateUrl: './connect-workspace.component.html', styleUrls: ['./connect-workspace.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class ConnectWorkspaceComponent { +export class ConnectWorkspaceComponent implements OnInit, OnDestroy { connectService = inject(ConnectWorkspaceService); - form = new FormGroup( - { - path: new FormControl('', [Validators.required]), - }, - { updateOn: 'submit' } - ); + workspaceManagerApiService = inject(WorkspaceManagerApiService); + + directories$ = new BehaviorSubject([]); + path$ = new BehaviorSubject(null); + pathSeparator$ = new BehaviorSubject(null); + isAngularWorkspace$ = new BehaviorSubject(false); + private destroyed$ = new Subject(); + + ngOnInit(): void { + this.workspaceManagerApiService + .getPathSeparator() + .subscribe((separator) => { + this.pathSeparator$.next(separator); + this.initWorkspacePath(); + }); + + combineLatest([this.path$, this.pathSeparator$]) + .pipe( + switchMap(([path, separator]) => { + path && separator && this.setIsAngularWorkspace(path, separator); + return path + ? this.workspaceManagerApiService.getDirectoriesInPath(path) + : of([]); + }), + takeUntil(this.destroyed$) + ) + .subscribe((directories) => { + this.directories$.next(directories); + }); + } + + ngOnDestroy(): void { + this.destroyed$.next(true); + } connectWorkspace(): void { - if (!this.form.valid) return; - const path = this.form.controls.path.value ?? ''; - this.connectService.connectWorkspace(path).subscribe(); + const path = this.path$.getValue(); + if (path) { + this.connectService.connectWorkspace(path).subscribe(); + } + } + + onPathChanged(path: string): void { + if (path !== this.path$.getValue()) { + this.path$.next(path); + } + } + + private initWorkspacePath(): void { + const currentWorkspacePath = sessionStorage.getItem(CURRENT_WORKSPACE_PATH); + const path$ = currentWorkspacePath + ? of(currentWorkspacePath) + : this.workspaceManagerApiService.getHomeDir(); + path$.subscribe((path: string) => this.path$.next(path)); + } + + private setIsAngularWorkspace(path: string, separator: string): void { + const parentPath = this.getParentPath(path, separator); + this.workspaceManagerApiService + .getDirectoriesInPath(parentPath) + .subscribe((directories) => { + const dir = directories.find( + (d: Directory) => `${parentPath}${separator}${d.name}` === path + ); + this.isAngularWorkspace$.next(dir?.isNG ?? false); + }); + } + + private getParentPath(path: string, separator: string): string { + const pathParts = path.split(separator); + return pathParts.slice(0, pathParts.length - 1).join(separator); } } diff --git a/libs/workspace-manager/src/lib/mocks/connect-workspace.mock.ts b/libs/workspace-manager/src/lib/mocks/connect-workspace.mock.ts new file mode 100644 index 0000000..5b58ec2 --- /dev/null +++ b/libs/workspace-manager/src/lib/mocks/connect-workspace.mock.ts @@ -0,0 +1,46 @@ +import { Directory } from '@angular-cli-gui/shared/data'; +import { ConnectWorkspaceService } from '@angular-cli-gui/workspace-manager'; +import { of } from 'rxjs'; + +import { WorkspaceManagerApiService } from '../data-access/workspace-manager-api.service'; + +export const PATH_SEPARATOR_MOCK = '/'; +export const HOMEDIR_PATH_MOCK = '/home'; +export const DIRECTORIES_MOCK: Directory[] = [ + { + name: 'dir a', + isNG: false, + }, + { + name: 'dir b', + isNG: true, + }, + { + name: 'dir c', + isNG: false, + }, +]; + +export type WorkspaceManagerApiServiceMock = Partial< + Record +>; + +export const getWorkspaceManagerApiServiceMock = + (): WorkspaceManagerApiServiceMock => { + return { + getPathSeparator: jest.fn(() => of(PATH_SEPARATOR_MOCK)), + getHomeDir: jest.fn(() => of(HOMEDIR_PATH_MOCK)), + getDirectoriesInPath: jest.fn(() => of(DIRECTORIES_MOCK)), + }; + }; + +export type ConnectWorkspaceServiceMock = Partial< + Record +>; + +export const getConnectWorkspaceServiceMock = + (): ConnectWorkspaceServiceMock => { + return { + connectWorkspace: jest.fn(() => of(null)), + }; + }; 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 index cb34111..9a2fe9c 100644 --- 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 @@ -11,3 +11,7 @@
{{ 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 index 27ecc9c..8af19f7 100644 --- 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 @@ -1,12 +1,18 @@ -.path { - display: flex; -} - -.path-part { +:host { display: flex; align-items: center; -} -.path-part-button { - margin-left: 2px; + .path { + display: flex; + flex: 2; + + .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.ts b/libs/workspace-manager/src/lib/ui/filesystem-navigator/filesystem-navigator-toolbar/filesystem-navigator-toolbar.component.ts index 4800abd..d1a461e 100644 --- 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 @@ -25,9 +25,11 @@ export class FilesystemNavigatorToolbarComponent implements OnChanges { pathParts: string[] = []; ngOnChanges(): void { - this.pathParts = (this.path as string) - .split(this.separator as string) - .filter((v) => !!v); + if (this.path) { + this.pathParts = (this.path as string) + .split(this.separator as string) + .filter(Boolean); + } } onPartClicked(pathPart: string): void { 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 index 65984b1..2b44055 100644 --- 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 @@ -2,7 +2,9 @@ [path]="path" [separator]="separator" (pathChange)="onPathChanged($event)" -> +> + +
diff --git a/libs/workspace-manager/tsconfig.lib.json b/libs/workspace-manager/tsconfig.lib.json index c6d50ba..f999a3d 100644 --- a/libs/workspace-manager/tsconfig.lib.json +++ b/libs/workspace-manager/tsconfig.lib.json @@ -11,7 +11,8 @@ "src/test-setup.ts", "src/**/*.spec.ts", "jest.config.ts", - "src/**/*.test.ts" + "src/**/*.test.ts", + "src/lib/mocks/**" ], "include": ["src/**/*.ts"] } diff --git a/libs/workspace-manager/tsconfig.spec.json b/libs/workspace-manager/tsconfig.spec.json index f6a7d97..0c8bd81 100644 --- a/libs/workspace-manager/tsconfig.spec.json +++ b/libs/workspace-manager/tsconfig.spec.json @@ -10,6 +10,7 @@ "jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", - "src/**/*.d.ts" + "src/**/*.d.ts", + "src/**/*.mock.ts" ] }