Skip to content

Commit d260f16

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

17 files changed

+293
-28
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { extractValueObject } from './extract -value-object';
2+
3+
describe('Extract value object', () => {
4+
const data = {
5+
name: 'value',
6+
nestedData: {
7+
name: 'nestedValue',
8+
level1: {
9+
level2: {
10+
level2Name: 'level2NameValue',
11+
level2Param: 'level2ParamValue',
12+
},
13+
},
14+
},
15+
};
16+
17+
it('should extract data from object', () => {
18+
expect(extractValueObject(data, 'nestedData.name')).toEqual({
19+
'nestedData.name': 'nestedValue',
20+
});
21+
expect(
22+
extractValueObject(data, 'nestedData.level1.level2.level2Name')
23+
).toEqual({ 'nestedData.level1.level2.level2Name': 'level2NameValue' });
24+
expect(extractValueObject(data, 'nestedData.level1')).toEqual({
25+
'nestedData.level1': {
26+
level2: {
27+
level2Name: 'level2NameValue',
28+
level2Param: 'level2ParamValue',
29+
},
30+
},
31+
});
32+
});
33+
34+
it('should extract data from object and override name', () => {
35+
expect(extractValueObject(data, 'nestedData.name', 'name')).toEqual({
36+
name: 'nestedValue',
37+
});
38+
expect(
39+
extractValueObject(
40+
data,
41+
'nestedData.level1.level2.level2Name',
42+
'level2Name'
43+
)
44+
).toEqual({ level2Name: 'level2NameValue' });
45+
expect(extractValueObject(data, 'nestedData.level1', 'level1')).toEqual({
46+
level1: {
47+
level2: {
48+
level2Name: 'level2NameValue',
49+
level2Param: 'level2ParamValue',
50+
},
51+
},
52+
});
53+
});
54+
55+
it('should extract data from object with wildcard', () => {
56+
expect(extractValueObject(data, 'nestedData.*')).toEqual({
57+
level1: {
58+
level2: {
59+
level2Name: 'level2NameValue',
60+
level2Param: 'level2ParamValue',
61+
},
62+
},
63+
name: 'nestedValue',
64+
});
65+
});
66+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { JsonObject, JsonValue } from '@angular-devkit/core/src/json/utils';
2+
3+
interface JsonObjectNested {
4+
[prop: string]: JsonValue | JsonObject | JsonObjectNested | undefined;
5+
}
6+
export const extractValueObject = <T>(
7+
objectData: T,
8+
path: string,
9+
overridePath?: string
10+
): JsonObject => {
11+
const traverse = (
12+
dataObject: JsonObjectNested,
13+
splitPaths: string[],
14+
currentPathIndex: number,
15+
defaultName = ''
16+
): void => {
17+
if (currentPathIndex === parts.length) {
18+
result[defaultName || splitPaths.join('.')] = dataObject as JsonValue;
19+
return;
20+
}
21+
if (!dataObject || typeof dataObject !== 'object') {
22+
return;
23+
}
24+
if (parts[currentPathIndex] === '*') {
25+
Object.entries(dataObject).forEach(([key, value]) =>
26+
traverse(
27+
value as JsonObjectNested,
28+
splitPaths.concat(key),
29+
currentPathIndex + 1,
30+
key
31+
)
32+
);
33+
return;
34+
}
35+
if (parts[currentPathIndex] in dataObject) {
36+
traverse(
37+
dataObject[parts[currentPathIndex]] as JsonObjectNested,
38+
splitPaths.concat(parts[currentPathIndex]),
39+
currentPathIndex + 1,
40+
defaultName
41+
);
42+
}
43+
};
44+
45+
const result: NonNullable<JsonObject> = {},
46+
parts = path.split('.');
47+
traverse(objectData as JsonObject, [], 0, overridePath);
48+
return result;
49+
};
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

+26
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { NodeJsSyncHost } from '@angular-devkit/core/node';
2+
import { JsonObject } from '@angular-devkit/core/src/json/utils';
23
import {
34
createWorkspaceHost,
45
readWorkspace as devKitReadWorkspace,
@@ -15,15 +16,20 @@ import {
1516
Logger,
1617
NotFoundException,
1718
} from '@nestjs/common';
19+
import { JSONSchema7 } from 'json-schema';
20+
import * as angularSchema from 'node_modules/@angular/cli/lib/config/schema.json';
1821

1922
import { SessionService } from '../session/session.service';
23+
import { extractValueObject } from '../utils/extract -value-object';
2024

2125
import { ProjectDto, UpdateProjectDto } from './dto';
26+
import { SchemaDto } from './dto/schema.dto';
2227
import {
2328
ANGULAR_WORKSPACE_NOT_FOUND_EXCEPTION,
2429
BAD_PATH_EXCEPTION,
2530
NOT_ANGULAR_WORKSPACE_EXCEPTION,
2631
} from './entities';
32+
import { configurationsPaths } from './entities/workspace-configuration';
2733

2834
const ANGULAR_JSON = '/angular.json';
2935

@@ -157,4 +163,24 @@ export class WorkspaceService {
157163
return new InternalServerErrorException(err);
158164
}
159165
}
166+
167+
getWorkspaceConfiguration(): Partial<JSONSchema7> {
168+
const configuration = configurationsPaths.reduce(
169+
(state: JsonObject, currentPath) => {
170+
const schemaValues = extractValueObject(
171+
angularSchema,
172+
currentPath.path,
173+
currentPath.overridePath
174+
);
175+
return { ...state, ...schemaValues };
176+
},
177+
{} as JsonObject
178+
);
179+
180+
return new SchemaDto({
181+
title: 'Angular CLI Workspace Configuration',
182+
description: 'Browser target options',
183+
properties: { ...(configuration as JSONSchema7['properties']) },
184+
});
185+
}
160186
}

‎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+
<p>Edit project configuration</p>
2+
3+
<ng-container *ngIf="formly$ | async as formly">
4+
<form [formGroup]="form" (ngSubmit)="onSubmit()">
5+
<formly-form
6+
[model]="formly.data"
7+
[fields]="formly.field"
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,11 +1,75 @@
11
import { CommonModule } from '@angular/common';
2-
import { Component } from '@angular/core';
2+
import { Component, OnInit } from '@angular/core';
3+
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
4+
import { Project } from '@angular-cli-gui/shared/data';
5+
import { JsonObject } from '@angular-devkit/core/src/json/utils';
6+
import { FormlyFieldConfig, FormlyModule } from '@ngx-formly/core';
7+
import { FormlyJsonschema } from '@ngx-formly/core/json-schema';
8+
import { FormlyMaterialModule } from '@ngx-formly/material';
9+
import { combineLatest, map, Subject, switchMap, tap } from 'rxjs';
10+
11+
import { WorkspaceSettingsService } from '../services';
312

413
@Component({
514
selector: 'cli-configuration',
615
standalone: true,
7-
imports: [CommonModule],
16+
imports: [
17+
CommonModule,
18+
FormlyModule,
19+
FormlyMaterialModule,
20+
ReactiveFormsModule,
21+
],
822
templateUrl: './configuration.component.html',
923
styleUrls: ['./configuration.component.css'],
1024
})
11-
export class ConfigurationComponent {}
25+
export class ConfigurationComponent implements OnInit {
26+
private formFields$ = new Subject<FormlyFieldConfig[]>();
27+
private formData$ = new Subject<JsonObject>();
28+
private projectName = '';
29+
30+
formly$ = combineLatest({
31+
data: this.formData$,
32+
field: this.formFields$,
33+
});
34+
35+
form = this.fb.group({});
36+
37+
constructor(
38+
private workspaceSettingsService: WorkspaceSettingsService,
39+
private fb: FormBuilder,
40+
private schema: FormlyJsonschema
41+
) {}
42+
43+
ngOnInit(): void {
44+
this.workspaceSettingsService
45+
.readWorkspaceProjectConfiguration()
46+
.subscribe((configuration) => {
47+
const filedGroup = this.schema.toFieldConfig(configuration)
48+
.fieldGroup as FormlyFieldConfig[];
49+
this.formFields$.next(filedGroup);
50+
});
51+
this.workspaceSettingsService
52+
.readWorkspaceProjectNames()
53+
.pipe(
54+
map((names) => names[0]),
55+
tap((projectName) => (this.projectName = projectName)),
56+
switchMap((currentProjectName) =>
57+
this.workspaceSettingsService.readWorkspaceProject(
58+
currentProjectName as string
59+
)
60+
)
61+
)
62+
.subscribe((formData) => {
63+
this.formData$.next(formData);
64+
});
65+
}
66+
67+
onSubmit(): void {
68+
this.workspaceSettingsService
69+
.updateWorkspaceProjectConfiguration(
70+
this.projectName,
71+
this.form.value as Project
72+
)
73+
.subscribe();
74+
}
75+
}
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+
}

0 commit comments

Comments
 (0)