Skip to content

Commit 199e799

Browse files
committed
feat: 🎸 edit project configuration
✅ Closes: 15
1 parent 7ec472b commit 199e799

18 files changed

+308
-31
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { JSONSchema7 } from 'json-schema';
2+
3+
export class SchemaDto implements JSONSchema7 {
4+
properties: JSONSchema7['properties'];
5+
title: JSONSchema7['title'];
6+
description: JSONSchema7['title'];
7+
8+
constructor({ title, properties, description }: JSONSchema7) {
9+
this.properties = properties;
10+
this.title = title;
11+
this.description = description;
12+
}
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export interface ExtractValuePaths {
2+
path: string;
3+
overridePath?: string;
4+
}
5+
export const configurationsPaths: ExtractValuePaths[] = [
6+
{ path: 'definitions.project.properties.prefix', overridePath: 'prefix' },
7+
{ path: 'definitions.project.properties.root', overridePath: 'root' },
8+
{
9+
path: 'definitions.project.properties.sourceRoot',
10+
overridePath: 'sourceRoot',
11+
},
12+
];

‎apps/cli-daemon/src/app/workspace/workspace.controller.ts

+6
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
Patch,
1010
Post,
1111
} from '@nestjs/common';
12+
import { JSONSchema7 } from 'json-schema';
1213

1314
import { ExecResult } from '../generators/dto';
1415
import { GeneratorsService } from '../generators/generators.service';
@@ -60,6 +61,11 @@ export class WorkspaceController {
6061
return this.workspaceService.readWorkspaceProjectNames();
6162
}
6263

64+
@Get('workspace-configuration')
65+
async getWorkspaceConfiguration(): Promise<JSONSchema7> {
66+
return this.workspaceService.getWorkspaceConfiguration();
67+
}
68+
6369
@Get('project/:projectName')
6470
async getProject(
6571
@Param('projectName') projectName: string

‎apps/cli-daemon/src/app/workspace/workspace.service.ts

+18
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,14 @@ import {
1515
Logger,
1616
NotFoundException,
1717
} from '@nestjs/common';
18+
import { JSONSchema7 } from 'json-schema';
19+
import { Draft07 } from 'json-schema-library';
20+
import * as angularSchema from 'node_modules/@angular/cli/lib/config/schema.json';
1821

1922
import { SessionService } from '../session/session.service';
2023

2124
import { ProjectDto, UpdateProjectDto } from './dto';
25+
import { SchemaDto } from './dto/schema.dto';
2226
import {
2327
ANGULAR_WORKSPACE_NOT_FOUND_EXCEPTION,
2428
BAD_PATH_EXCEPTION,
@@ -157,4 +161,18 @@ export class WorkspaceService {
157161
return new InternalServerErrorException(err);
158162
}
159163
}
164+
165+
getWorkspaceConfiguration(): Partial<JSONSchema7> {
166+
const jsonSchema = new Draft07(angularSchema);
167+
168+
const { root, prefix, sourceRoot } =
169+
jsonSchema.getSchema()['definitions']['project']['properties'];
170+
const properties = new Draft07({ root, prefix, sourceRoot }).getSchema();
171+
172+
return new SchemaDto({
173+
title: 'Angular CLI Workspace Configuration',
174+
description: 'Browser target options',
175+
properties: properties,
176+
});
177+
}
160178
}

‎apps/cli-daemon/tsconfig.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"noImplicitOverride": true,
1919
"noPropertyAccessFromIndexSignature": true,
2020
"noImplicitReturns": true,
21-
"noFallthroughCasesInSwitch": true
21+
"noFallthroughCasesInSwitch": true,
22+
"resolveJsonModule": true
2223
}
2324
}
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import { CommonModule } from '@angular/common';
22
import { ChangeDetectionStrategy, Component } from '@angular/core';
33

4-
import { WorkspaceSettingsService } from './workspace-settings.service';
5-
64
@Component({
75
selector: 'cli-workspace-settings',
86
standalone: true,
@@ -11,10 +9,4 @@ import { WorkspaceSettingsService } from './workspace-settings.service';
119
styleUrls: ['./workspace-settings.component.scss'],
1210
changeDetection: ChangeDetectionStrategy.OnPush,
1311
})
14-
export class WorkspaceSettingsComponent {
15-
angularJson$ = this.workspaceSettingsService.readWorkspaceProjectNames();
16-
17-
constructor(
18-
private readonly workspaceSettingsService: WorkspaceSettingsService
19-
) {}
20-
}
12+
export class WorkspaceSettingsComponent {}

‎apps/cli-gui/src/app/workspace-settings/workspace-settings.service.ts

-14
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -1 +1,12 @@
1-
<p>configuration works!</p>
1+
<h1>Edit project configuration</h1>
2+
3+
<ng-container *ngIf="formly$ | async as formly">
4+
<form [formGroup]="form" (ngSubmit)="onSubmit()">
5+
<formly-form
6+
[model]="formly.formData"
7+
[fields]="formly.formFields"
8+
[form]="form"
9+
></formly-form>
10+
<button mat-button type="submit">Submit</button>
11+
</form>
12+
</ng-container>
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,58 @@
11
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
3+
4+
import {
5+
createWorkspaceSettingsServiceMockk,
6+
workspaceConfigurationMock,
7+
} from '../../testing';
8+
import { WorkspaceSettingsService } from '../services';
29

310
import { ConfigurationComponent } from './configuration.component';
411

512
describe('ConfigurationComponent', () => {
613
let component: ConfigurationComponent;
714
let fixture: ComponentFixture<ConfigurationComponent>;
15+
let service: WorkspaceSettingsService;
816

917
beforeEach(async () => {
1018
await TestBed.configureTestingModule({
11-
imports: [ConfigurationComponent],
19+
imports: [ConfigurationComponent, NoopAnimationsModule],
20+
providers: [
21+
{
22+
provide: WorkspaceSettingsService,
23+
useValue: createWorkspaceSettingsServiceMockk(),
24+
},
25+
],
1226
}).compileComponents();
1327

1428
fixture = TestBed.createComponent(ConfigurationComponent);
1529
component = fixture.componentInstance;
1630
fixture.detectChanges();
31+
service = TestBed.inject(WorkspaceSettingsService);
1732
});
1833

1934
it('should create', () => {
2035
expect(component).toBeTruthy();
2136
});
37+
38+
it('should formly create descriptions for fields', () => {
39+
const { root, sourceRoot, prefix } = workspaceConfigurationMock.properties;
40+
const innerHTML = fixture.debugElement.nativeElement.innerHTML;
41+
expect(innerHTML).toContain(root.description);
42+
expect(innerHTML).toContain(sourceRoot.description);
43+
expect(innerHTML).toContain(prefix.description);
44+
});
45+
46+
it('should submit form', () => {
47+
const updateWorkspaceProjectConfigurationSpy = jest.spyOn(
48+
service,
49+
'updateWorkspaceProjectConfiguration'
50+
);
51+
fixture.debugElement.nativeElement.querySelector('button').click();
52+
expect(updateWorkspaceProjectConfigurationSpy).toBeCalledWith('name', {
53+
prefix: 'prefix',
54+
root: 'root',
55+
sourceRoot: 'sourceRoot',
56+
});
57+
});
2258
});
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,82 @@
1-
import { CommonModule } from '@angular/common';
2-
import { Component } from '@angular/core';
1+
import { AsyncPipe, NgIf } from '@angular/common';
2+
import { ChangeDetectionStrategy, Component } from '@angular/core';
3+
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
4+
import { Project } from '@angular-cli-gui/shared/data';
5+
import { FormlyFieldConfig, FormlyModule } from '@ngx-formly/core';
6+
import { FormlyJsonschema } from '@ngx-formly/core/json-schema';
7+
import { FormlyMaterialModule } from '@ngx-formly/material';
8+
import {
9+
combineLatest,
10+
distinctUntilChanged,
11+
map,
12+
shareReplay,
13+
switchMap,
14+
} from 'rxjs';
15+
16+
import { WorkspaceSettingsService } from '../services';
317

418
@Component({
519
selector: 'cli-configuration',
620
standalone: true,
7-
imports: [CommonModule],
21+
imports: [
22+
NgIf,
23+
AsyncPipe,
24+
FormlyModule,
25+
FormlyMaterialModule,
26+
ReactiveFormsModule,
27+
],
828
templateUrl: './configuration.component.html',
929
styleUrls: ['./configuration.component.css'],
30+
changeDetection: ChangeDetectionStrategy.OnPush,
1031
})
11-
export class ConfigurationComponent {}
32+
export class ConfigurationComponent {
33+
private readonly projectConfiguration$ = this.workspaceSettingsService
34+
.readWorkspaceProjectConfiguration()
35+
.pipe(
36+
map(
37+
(configuration) =>
38+
this.schema.toFieldConfig(configuration)
39+
.fieldGroup as FormlyFieldConfig[]
40+
)
41+
);
42+
43+
private readonly projectName$ = this.workspaceSettingsService
44+
.readWorkspaceProjectNames()
45+
.pipe(
46+
map((names) => names[0]),
47+
shareReplay({ bufferSize: 1, refCount: true })
48+
);
49+
50+
private readonly workspaceProject$ = this.projectName$.pipe(
51+
switchMap((currentProjectName: string) =>
52+
this.workspaceSettingsService.readWorkspaceProject(currentProjectName)
53+
),
54+
distinctUntilChanged()
55+
);
56+
57+
readonly formly$ = combineLatest([
58+
this.projectConfiguration$,
59+
this.workspaceProject$,
60+
]).pipe(map(([formFields, formData]) => ({ formFields, formData })));
61+
62+
form = this.fb.group({});
63+
64+
constructor(
65+
private workspaceSettingsService: WorkspaceSettingsService,
66+
private fb: FormBuilder,
67+
private schema: FormlyJsonschema
68+
) {}
69+
70+
onSubmit(): void {
71+
this.projectName$
72+
.pipe(
73+
switchMap((projectName) => {
74+
return this.workspaceSettingsService.updateWorkspaceProjectConfiguration(
75+
projectName,
76+
this.form.value as Project
77+
);
78+
})
79+
)
80+
.subscribe();
81+
}
82+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './workspace-settings';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './workspace-settings.service';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { HttpClient } from '@angular/common/http';
2+
import { Injectable } from '@angular/core';
3+
import { Project } from '@angular-cli-gui/shared/data';
4+
import { JsonObject } from '@angular-devkit/core/src/json/utils';
5+
import { JSONSchema7 } from 'json-schema';
6+
import { Observable } from 'rxjs';
7+
8+
@Injectable({
9+
providedIn: 'root',
10+
})
11+
export class WorkspaceSettingsService {
12+
constructor(private readonly http: HttpClient) {}
13+
14+
readWorkspaceProjectNames(): Observable<string[]> {
15+
return this.http.get<string[]>(`/api/workspace/project-names`);
16+
}
17+
18+
readWorkspaceProject(projectName: string): Observable<JsonObject> {
19+
return this.http.get<JsonObject>(`/api/workspace/project/${projectName}`);
20+
}
21+
22+
readWorkspaceProjectConfiguration(): Observable<JSONSchema7> {
23+
return this.http.get<JSONSchema7>(`/api/workspace/workspace-configuration`);
24+
}
25+
26+
updateWorkspaceProjectConfiguration(
27+
projectName: string,
28+
projectData: Project
29+
): Observable<Project> {
30+
return this.http.patch<Project>(`/api/workspace/project/${projectName}`, {
31+
...projectData,
32+
});
33+
}
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './workspace-settings.service.mock';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { BehaviorSubject, Observable, Subject } from 'rxjs';
2+
3+
import { WorkspaceSettingsService } from '../lib/services';
4+
5+
type WorkspaceSettingsServiceMock = Partial<
6+
Record<keyof WorkspaceSettingsService, jest.Mock<any, any> | Observable<any>>
7+
>;
8+
9+
export const workspaceConfigurationMock = {
10+
properties: {
11+
root: {
12+
type: 'string',
13+
description: 'Root of the project files.',
14+
},
15+
prefix: {
16+
type: 'string',
17+
format: 'html-selector',
18+
description: 'The prefix to apply to generated selectors.',
19+
},
20+
sourceRoot: {
21+
type: 'string',
22+
description:
23+
'The root of the source files, assets and index.html file structure.',
24+
},
25+
},
26+
title: 'Angular CLI Workspace Configuration',
27+
description: 'Browser target options',
28+
};
29+
30+
export const workspaceNamesMock = ['name'];
31+
32+
export const workspaceProjectMock = {
33+
root: 'root',
34+
prefix: 'prefix',
35+
sourceRoot: 'sourceRoot',
36+
extensions: {
37+
projectType: 'application',
38+
schematics: {
39+
'@schematics/angular:component': {
40+
style: 'scss',
41+
},
42+
},
43+
},
44+
};
45+
46+
export function createWorkspaceSettingsServiceMockk(): WorkspaceSettingsServiceMock {
47+
return {
48+
readWorkspaceProject: jest.fn(
49+
() => new BehaviorSubject(workspaceProjectMock)
50+
),
51+
readWorkspaceProjectNames: jest.fn(
52+
() => new BehaviorSubject(workspaceNamesMock)
53+
),
54+
readWorkspaceProjectConfiguration: jest.fn(
55+
() => new BehaviorSubject(workspaceConfigurationMock)
56+
),
57+
updateWorkspaceProjectConfiguration: jest.fn(() => new Subject()),
58+
};
59+
}

0 commit comments

Comments
 (0)