Skip to content

Commit 15949c3

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

17 files changed

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

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

1921
import { SessionService } from '../session/session.service';
22+
import { extractValueObject } from '../utils/extract-value-object';
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,
2529
NOT_ANGULAR_WORKSPACE_EXCEPTION,
2630
} from './entities';
31+
import { configurationsPaths } from './entities/workspace-configuration';
2732

2833
const ANGULAR_JSON = '/angular.json';
2934

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

‎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.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,11 +1,95 @@
1-
import { CommonModule } from '@angular/common';
2-
import { Component } from '@angular/core';
1+
import { AsyncPipe, NgIf } from '@angular/common';
2+
import { ChangeDetectionStrategy, 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 {
10+
combineLatest,
11+
forkJoin,
12+
map,
13+
Observable,
14+
Subject,
15+
switchMap,
16+
tap,
17+
} from 'rxjs';
18+
19+
import { WorkspaceSettingsService } from '../services';
320

421
@Component({
522
selector: 'cli-configuration',
623
standalone: true,
7-
imports: [CommonModule],
24+
imports: [
25+
NgIf,
26+
AsyncPipe,
27+
FormlyModule,
28+
FormlyMaterialModule,
29+
ReactiveFormsModule,
30+
],
831
templateUrl: './configuration.component.html',
932
styleUrls: ['./configuration.component.css'],
33+
changeDetection: ChangeDetectionStrategy.OnPush,
1034
})
11-
export class ConfigurationComponent {}
35+
export class ConfigurationComponent implements OnInit {
36+
private formFields$ = new Subject<FormlyFieldConfig[]>();
37+
private formData$ = new Subject<JsonObject>();
38+
private projectName = '';
39+
40+
private readonly projectConfiguration$ = this.workspaceSettingsService
41+
.readWorkspaceProjectConfiguration()
42+
.pipe(
43+
map(
44+
(configuration) =>
45+
this.schema.toFieldConfig(configuration)
46+
.fieldGroup as FormlyFieldConfig[]
47+
)
48+
);
49+
50+
private readonly projectName$ = this.workspaceSettingsService
51+
.readWorkspaceProjectNames()
52+
.pipe(
53+
map((names) => names[0]),
54+
tap((projectName) => (this.projectName = projectName))
55+
);
56+
57+
private readonly workspaceProject$ = this.projectName$.pipe(
58+
switchMap((currentProjectName: string) =>
59+
this.workspaceSettingsService.readWorkspaceProject(currentProjectName)
60+
)
61+
);
62+
63+
readonly formly$: Observable<{
64+
formData: JsonObject;
65+
formFields: FormlyFieldConfig[];
66+
}> = combineLatest([this.formData$, this.formFields$]).pipe(
67+
map(([formData, formFields]) => ({ formFields, formData }))
68+
);
69+
70+
form = this.fb.group({});
71+
72+
constructor(
73+
private workspaceSettingsService: WorkspaceSettingsService,
74+
private fb: FormBuilder,
75+
private schema: FormlyJsonschema
76+
) {}
77+
78+
ngOnInit(): void {
79+
forkJoin([this.projectConfiguration$, this.workspaceProject$]).subscribe(
80+
([fields, projectData]) => {
81+
this.formFields$.next(fields);
82+
this.formData$.next(projectData);
83+
}
84+
);
85+
}
86+
87+
onSubmit(): void {
88+
this.workspaceSettingsService
89+
.updateWorkspaceProjectConfiguration(
90+
this.projectName,
91+
this.form.value as Project
92+
)
93+
.subscribe();
94+
}
95+
}
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';

0 commit comments

Comments
 (0)