From 95e49a6286ee2b83df20af9e85640a9a33b867cf Mon Sep 17 00:00:00 2001 From: Nina Date: Wed, 30 Oct 2024 08:23:59 +0100 Subject: [PATCH 01/50] feat(oblique/schematics): increase `maximumError` budget to 1.8mb to avoid error on build OUI-3342 --- projects/oblique/schematics/index/ng-add/rules/oblique.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/oblique/schematics/index/ng-add/rules/oblique.ts b/projects/oblique/schematics/index/ng-add/rules/oblique.ts index 6c40aac00..6fb159bc6 100644 --- a/projects/oblique/schematics/index/ng-add/rules/oblique.ts +++ b/projects/oblique/schematics/index/ng-add/rules/oblique.ts @@ -183,7 +183,7 @@ function raiseBuildBudget(): Rule { { type: 'initial', maximumWarning: '1.3mb', - maximumError: '1.5mb' + maximumError: '1.8mb' }, { type: 'anyComponentStyle', From cb7044994be49c44aa4e90ea1853d90652193409 Mon Sep 17 00:00:00 2001 From: Nina Date: Tue, 29 Oct 2024 11:07:41 +0100 Subject: [PATCH 02/50] feat(oblique/schematics): remove Angular version check for 'allowedCommonJsDependencies' config OUI-3343 --- .../schematics/index/ng-add/rules/obliqueFeatures.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/projects/oblique/schematics/index/ng-add/rules/obliqueFeatures.ts b/projects/oblique/schematics/index/ng-add/rules/obliqueFeatures.ts index a258ff1dd..3029d3573 100644 --- a/projects/oblique/schematics/index/ng-add/rules/obliqueFeatures.ts +++ b/projects/oblique/schematics/index/ng-add/rules/obliqueFeatures.ts @@ -13,7 +13,6 @@ import { appModulePath, applyChanges, createSrcFile, - getAngularVersion, getTemplate, routingModulePath } from '../ng-add-utils'; @@ -38,10 +37,8 @@ function addAjv(ajv: boolean): Rule { infoMigration(_context, 'Oblique feature: Adding schema validation'); addDevDependency(tree, 'ajv'); addDevDependency(tree, 'ajv-formats'); - if (getAngularVersion(tree) >= 10) { - addAngularConfigInList(tree, ['architect', 'build', 'options', 'allowedCommonJsDependencies'], 'ajv'); - addAngularConfigInList(tree, ['architect', 'build', 'options', 'allowedCommonJsDependencies'], 'ajv-formats'); - } + addAngularConfigInList(tree, ['architect', 'build', 'options', 'allowedCommonJsDependencies'], 'ajv'); + addAngularConfigInList(tree, ['architect', 'build', 'options', 'allowedCommonJsDependencies'], 'ajv-formats'); } return tree; }); From cfc0ca186751af7dcc71959df3ce6974414b16ec Mon Sep 17 00:00:00 2001 From: Nina Date: Tue, 29 Oct 2024 12:55:21 +0100 Subject: [PATCH 03/50] fix(oblique/schematics): add 'allowedCommonJsDependencies' config to avoid ajv warnings on build OUI-3343 --- .../oblique/schematics/index/ng-add/rules/obliqueFeatures.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/projects/oblique/schematics/index/ng-add/rules/obliqueFeatures.ts b/projects/oblique/schematics/index/ng-add/rules/obliqueFeatures.ts index 3029d3573..eb30b0191 100644 --- a/projects/oblique/schematics/index/ng-add/rules/obliqueFeatures.ts +++ b/projects/oblique/schematics/index/ng-add/rules/obliqueFeatures.ts @@ -17,7 +17,7 @@ import { routingModulePath } from '../ng-add-utils'; import {ObIOptionsSchema} from '../ng-add.model'; -import {ObliquePackage, addAngularConfigInList, addFile, createSafeRule, infoMigration} from '../../utils'; +import {ObliquePackage, addFile, createSafeRule, infoMigration, setOrCreateAngularProjectsConfig} from '../../utils'; export function obliqueFeatures(options: ObIOptionsSchema): Rule { return (tree: Tree, _context: SchematicContext) => @@ -37,9 +37,8 @@ function addAjv(ajv: boolean): Rule { infoMigration(_context, 'Oblique feature: Adding schema validation'); addDevDependency(tree, 'ajv'); addDevDependency(tree, 'ajv-formats'); - addAngularConfigInList(tree, ['architect', 'build', 'options', 'allowedCommonJsDependencies'], 'ajv'); - addAngularConfigInList(tree, ['architect', 'build', 'options', 'allowedCommonJsDependencies'], 'ajv-formats'); } + setOrCreateAngularProjectsConfig(tree, ['architect', 'build', 'options', 'allowedCommonJsDependencies'], ['ajv', 'ajv-formats']); return tree; }); } From 36ee8d48c5cf428568a0a454e318ad3405a3f126 Mon Sep 17 00:00:00 2001 From: Nina Date: Wed, 30 Oct 2024 09:59:37 +0100 Subject: [PATCH 04/50] feat(toolchain): harmonize package names OUI-3341 --- package-lock.json | 54 ++++++++++++++-------------- projects/sandbox-ssr/package.json | 2 +- projects/sandbox/package.json | 2 +- projects/sds/package.json | 2 +- projects/sds/schematics/package.json | 2 +- 5 files changed, 32 insertions(+), 30 deletions(-) diff --git a/package-lock.json b/package-lock.json index 507ba7793..a0050209c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4696,6 +4696,18 @@ "resolved": "projects/oblique", "link": true }, + "node_modules/@oblique/sandbox": { + "resolved": "projects/sandbox", + "link": true + }, + "node_modules/@oblique/sandbox-ssr": { + "resolved": "projects/sandbox-ssr", + "link": true + }, + "node_modules/@oblique/sds": { + "resolved": "projects/sds", + "link": true + }, "node_modules/@oblique/service-navigation-web-component": { "resolved": "projects/service-navigation-web-component", "link": true @@ -14352,14 +14364,6 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, - "node_modules/sandbox": { - "resolved": "projects/sandbox", - "link": true - }, - "node_modules/sandbox-ssr": { - "resolved": "projects/sandbox-ssr", - "link": true - }, "node_modules/sass": { "version": "1.77.6", "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.6.tgz", @@ -15546,10 +15550,6 @@ "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==", "dev": true }, - "node_modules/swiss-design-system": { - "resolved": "projects/sds", - "link": true - }, "node_modules/symbol-observable": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", @@ -17659,7 +17659,7 @@ }, "projects/cli": { "name": "@oblique/cli", - "version": "12.0.3", + "version": "12.0.4", "dependencies": { "@commander-js/extra-typings": "^12.1.0" }, @@ -17669,7 +17669,7 @@ }, "projects/design-system": { "name": "@oblique/design-system", - "version": "12.0.3", + "version": "12.0.4", "devDependencies": { "ng-packagr": "^18.2.1" }, @@ -17683,7 +17683,7 @@ }, "projects/oblique": { "name": "@oblique/oblique", - "version": "12.0.3", + "version": "12.0.4", "dependencies": { "@angular-eslint/schematics": "^18.4.2", "@angular/cdk": "^18.0.0 || ^19.0.0", @@ -17701,28 +17701,30 @@ "@angular/common": "^18.0.0 || ^19.0.0", "@angular/compiler": "^18.0.0 || ^19.0.0", "@angular/core": "^18.0.0 || ^19.0.0", - "@angular/forms": "^18.2.13", + "@angular/forms": "^18.0.0 || ^19.0.0", "@angular/material": "^18.0.0 || ^19.0.0", "@angular/platform-browser": "^18.0.0 || ^19.0.0", - "@angular/platform-browser-dynamic": "^18.2.13", - "@angular/router": "^18.2.13", - "@ngx-translate/core": "^15.0.0", - "@popperjs/core": "^2.11.8", + "@angular/platform-browser-dynamic": "^18.0.0 || ^19.0.0", + "@angular/router": "^18.0.0 || ^19.0.0", + "@ngx-translate/core": ">=15.0.0", + "@popperjs/core": "^2.0.0", "ajv": "^8.0.0", "ajv-formats": "^3.0.1", - "angular-oauth2-oidc": "^17.0.2", + "angular-oauth2-oidc": "^17.0.0", "jwt-decode": "^4.0.0", "rxjs": "^7.0.0" } }, "projects/sandbox": { - "version": "12.0.3", + "name": "@oblique/sandbox", + "version": "12.0.4", "dependencies": { "raw-loader": "^4.0.2" } }, "projects/sandbox-ssr": { - "version": "12.0.3", + "name": "@oblique/sandbox-ssr", + "version": "12.0.4", "dependencies": { "@angular/platform-server": "^18.2.13", "@angular/ssr": "^18.2.12", @@ -17733,8 +17735,8 @@ } }, "projects/sds": { - "name": "swiss-design-system", - "version": "12.0.3", + "name": "@oblique/sds", + "version": "12.0.4", "dependencies": { "highlight.js": "^11.10.0", "jquery": "^3.7.1", @@ -17747,7 +17749,7 @@ }, "projects/service-navigation-web-component": { "name": "@oblique/service-navigation-web-component", - "version": "12.0.3", + "version": "12.0.4", "devDependencies": { "@angular/elements": "^18.2.13" } diff --git a/projects/sandbox-ssr/package.json b/projects/sandbox-ssr/package.json index 2e591a35d..6e475aca7 100644 --- a/projects/sandbox-ssr/package.json +++ b/projects/sandbox-ssr/package.json @@ -1,5 +1,5 @@ { - "name": "sandbox-ssr", + "name": "@oblique/sandbox-ssr", "version": "12.0.4", "scripts": { "start": "ng serve", diff --git a/projects/sandbox/package.json b/projects/sandbox/package.json index 6a8c32fd9..be3713775 100644 --- a/projects/sandbox/package.json +++ b/projects/sandbox/package.json @@ -1,5 +1,5 @@ { - "name": "sandbox", + "name": "@oblique/sandbox", "version": "12.0.4", "scripts": { "start": "ng serve", diff --git a/projects/sds/package.json b/projects/sds/package.json index a1a897881..0a2ae4b2c 100644 --- a/projects/sds/package.json +++ b/projects/sds/package.json @@ -1,5 +1,5 @@ { - "name": "swiss-design-system", + "name": "@oblique/sds", "version": "12.0.4", "scripts": { "start": "ng serve", diff --git a/projects/sds/schematics/package.json b/projects/sds/schematics/package.json index 6ec4489f6..1673f3d54 100755 --- a/projects/sds/schematics/package.json +++ b/projects/sds/schematics/package.json @@ -1,5 +1,5 @@ { - "name": "@oblique/sds", + "name": "@oblique/sds-schematics", "version": "", "description": "Oblique SDS schematics", "scripts": { From 69793d3f38052cd98c9240514dbd03384910d0aa Mon Sep 17 00:00:00 2001 From: Olivier Percebois-Garve Date: Fri, 8 Nov 2024 11:14:38 +0100 Subject: [PATCH 05/50] fix(oblique/input-clear): ensure the control is visible when the field is in error OUI-3380 --- .../src/styles/scss/core/components/_text-control.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/projects/oblique/src/styles/scss/core/components/_text-control.scss b/projects/oblique/src/styles/scss/core/components/_text-control.scss index 8a481db65..a10f794cc 100644 --- a/projects/oblique/src/styles/scss/core/components/_text-control.scss +++ b/projects/oblique/src/styles/scss/core/components/_text-control.scss @@ -4,6 +4,10 @@ display: none; } +.mat-mdc-form-field.mat-form-field-invalid .ob-text-control-clear { + padding-right: variables.$ob-spacing-lg + variables.$ob-spacing-sm; +} + .ob-text-control { display: flex !important; From cb86d9cd49076f7e68435d36c4e9eba32fd5cf51 Mon Sep 17 00:00:00 2001 From: Olivier Percebois-Garve Date: Fri, 22 Nov 2024 00:10:55 +0100 Subject: [PATCH 06/50] fix(oblique/material): add padding before an input suffix This visually separates the suffix from what comes right before, like the input clear feature OUI-3380 --- .../oblique/src/styles/scss/material/_mat-form-field.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/projects/oblique/src/styles/scss/material/_mat-form-field.scss b/projects/oblique/src/styles/scss/material/_mat-form-field.scss index a0e999a8b..4b7f764eb 100644 --- a/projects/oblique/src/styles/scss/material/_mat-form-field.scss +++ b/projects/oblique/src/styles/scss/material/_mat-form-field.scss @@ -494,6 +494,10 @@ z-index: 1; } + .mat-mdc-form-field-text-suffix { + padding-left: core-variables.$ob-spacing-xs; + } + @at-root #{selector.append(".mat-mdc-form-field-type-mat-input", &)} { .mat-mdc-form-field-icon-prefix, .mat-mdc-form-field-icon-suffix { From 63e58783afd57cb741f4080f978797afbfc078fb Mon Sep 17 00:00:00 2001 From: Olivier Percebois-Garve Date: Wed, 13 Nov 2024 11:55:46 +0100 Subject: [PATCH 07/50] feat(sandbox/input-clear): demonstrate clear button with error icon and suffix OUI-3380 --- .../input-clear/input-clear.component.html | 24 ++++++++++++++++--- .../input-clear/input-clear.component.ts | 2 ++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/projects/sandbox/src/app/samples/input-clear/input-clear.component.html b/projects/sandbox/src/app/samples/input-clear/input-clear.component.html index f392b91d1..ce2f3fdea 100644 --- a/projects/sandbox/src/app/samples/input-clear/input-clear.component.html +++ b/projects/sandbox/src/app/samples/input-clear/input-clear.component.html @@ -6,14 +6,32 @@

Template-driven form

- Mandatory with input clear - - + + Minlength with input clear and prefix / suffix + + + Text Prefix + Text Suffix + +
Mandatory LG diff --git a/projects/sandbox/src/app/samples/input-clear/input-clear.component.ts b/projects/sandbox/src/app/samples/input-clear/input-clear.component.ts index 367f6122f..47fad31b4 100644 --- a/projects/sandbox/src/app/samples/input-clear/input-clear.component.ts +++ b/projects/sandbox/src/app/samples/input-clear/input-clear.component.ts @@ -7,6 +7,8 @@ import {FormControl, FormGroup, UntypedFormBuilder, UntypedFormGroup} from '@ang }) export class InputClearSampleComponent implements OnInit { mandatoryModel: string; + minlengthModel: string; + minlengthPrefixSuffixModel: string; mandatoryLgModel: string; mandatorySmModel: string; datepickerModel: string; From bc6df8e03282b03f426ea7d8551facdc9c50c46f Mon Sep 17 00:00:00 2001 From: Nina Date: Fri, 8 Nov 2024 10:36:16 +0100 Subject: [PATCH 08/50] feat(sds/component-page): add new component-page component OUI-3332 --- projects/sds/CONTRIBUTING.md | 1 + .../component-page.component.html | 4 +++ .../component-page.component.scss | 33 +++++++++++++++++++ .../component-page.component.spec.ts | 23 +++++++++++++ .../component-page.component.ts | 22 +++++++++++++ .../component-page/component-page.mapper.ts | 9 +++++ .../src/app/component-page/component-page.ts | 6 ++++ .../component-page/component-pages.routes.ts | 7 ++++ 8 files changed, 105 insertions(+) create mode 100644 projects/sds/src/app/component-page/component-page.component.html create mode 100644 projects/sds/src/app/component-page/component-page.component.scss create mode 100644 projects/sds/src/app/component-page/component-page.component.spec.ts create mode 100644 projects/sds/src/app/component-page/component-page.component.ts create mode 100644 projects/sds/src/app/component-page/component-page.mapper.ts create mode 100644 projects/sds/src/app/component-page/component-page.ts create mode 100644 projects/sds/src/app/component-page/component-pages.routes.ts diff --git a/projects/sds/CONTRIBUTING.md b/projects/sds/CONTRIBUTING.md index c777286f2..513bdec63 100644 --- a/projects/sds/CONTRIBUTING.md +++ b/projects/sds/CONTRIBUTING.md @@ -52,4 +52,5 @@ All commits related to the SDS package must use the **sds** package and 1 of the - **styles** - **tabbed-page** - **text-page** +- **component-page** - **toolchain** diff --git a/projects/sds/src/app/component-page/component-page.component.html b/projects/sds/src/app/component-page/component-page.component.html new file mode 100644 index 000000000..7cb622115 --- /dev/null +++ b/projects/sds/src/app/component-page/component-page.component.html @@ -0,0 +1,4 @@ +
+

{{ componentToLoad?.title }}

+
+ diff --git a/projects/sds/src/app/component-page/component-page.component.scss b/projects/sds/src/app/component-page/component-page.component.scss new file mode 100644 index 000000000..d083afa3c --- /dev/null +++ b/projects/sds/src/app/component-page/component-page.component.scss @@ -0,0 +1,33 @@ +@use "../../styles/scss/core/mixins/layout"; + +:host { + grid-column-start: 2; + grid-row-start: 1; + min-height: calc(100vh - 150px); + border-radius: 4px; + background-color: rgb(255, 255, 255); + border-width: 1px; + border-style: solid; + border-color: #e5e7eb; + display: flex; + flex-direction: column; + justify-content: flex-start; + overflow-x: auto; + padding: 4rem 20px; +} +@include layout.ob-media-breakpoint-down(md) { + :host { + margin-top: 104px; // free space for the header, with 20px padding-like space around the logo + border-radius: 0; + } +} + +h1 { + padding-top: 0.75rem; + padding-left: 0.75rem; +} + +.title-container { + display: flex; + gap: 16px; +} diff --git a/projects/sds/src/app/component-page/component-page.component.spec.ts b/projects/sds/src/app/component-page/component-page.component.spec.ts new file mode 100644 index 000000000..b8584b6ee --- /dev/null +++ b/projects/sds/src/app/component-page/component-page.component.spec.ts @@ -0,0 +1,23 @@ +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {RouterTestingModule} from '@angular/router/testing'; +import {ComponentPageComponent} from './component-page.component'; + +describe(ComponentPageComponent.name, () => { + let component: ComponentPageComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, RouterTestingModule, ComponentPageComponent] + }).compileComponents(); + + fixture = TestBed.createComponent(ComponentPageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/projects/sds/src/app/component-page/component-page.component.ts b/projects/sds/src/app/component-page/component-page.component.ts new file mode 100644 index 000000000..9db37d369 --- /dev/null +++ b/projects/sds/src/app/component-page/component-page.component.ts @@ -0,0 +1,22 @@ +import {CommonModule, NgComponentOutlet} from '@angular/common'; +import {Component, OnInit, inject} from '@angular/core'; +import {Router} from '@angular/router'; +import {ComponentPage} from './component-page'; +import {ComponentPageMapper} from './component-page.mapper'; + +@Component({ + selector: 'app-component-page', + templateUrl: './component-page.component.html', + styleUrls: ['./component-page.component.scss'], + standalone: true, + imports: [CommonModule, NgComponentOutlet] +}) +export class ComponentPageComponent implements OnInit { + public componentToLoad: ComponentPage; + private readonly router = inject(Router); + + ngOnInit(): void { + const componentName = this.router.url.split('?')[0].split('/').pop() ?? ''; + this.componentToLoad = ComponentPageMapper.getComponentPageComponent(componentName); + } +} diff --git a/projects/sds/src/app/component-page/component-page.mapper.ts b/projects/sds/src/app/component-page/component-page.mapper.ts new file mode 100644 index 000000000..91fa307fd --- /dev/null +++ b/projects/sds/src/app/component-page/component-page.mapper.ts @@ -0,0 +1,9 @@ +import {ComponentPage} from './component-page'; + +export class ComponentPageMapper { + private static readonly components: Record = {}; + + static getComponentPageComponent(name: string): ComponentPage | undefined { + return this.components[name]; + } +} diff --git a/projects/sds/src/app/component-page/component-page.ts b/projects/sds/src/app/component-page/component-page.ts new file mode 100644 index 000000000..31141c762 --- /dev/null +++ b/projects/sds/src/app/component-page/component-page.ts @@ -0,0 +1,6 @@ +import {Type} from '@angular/core'; + +export interface ComponentPage { + title: string; + component: Type; +} diff --git a/projects/sds/src/app/component-page/component-pages.routes.ts b/projects/sds/src/app/component-page/component-pages.routes.ts new file mode 100644 index 000000000..160b001cc --- /dev/null +++ b/projects/sds/src/app/component-page/component-pages.routes.ts @@ -0,0 +1,7 @@ +import {ComponentPageComponent} from './component-page.component'; + +// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment +export default [ + {path: '', component: ComponentPageComponent}, + {path: '**', redirectTo: 'introductions/welcome'} +]; From ef7056e804c011bb3459fa3768440b77ada4cdb9 Mon Sep 17 00:00:00 2001 From: Nina Date: Fri, 22 Nov 2024 12:51:23 +0100 Subject: [PATCH 09/50] feat(sds/component-page): add newsletter OUI-3332 --- projects/sds/src/app.routes.ts | 4 + .../component-page/component-page.mapper.ts | 8 +- .../newsletter/newsletter-token.model.ts | 8 ++ .../newsletter/newsletter.component.html | 16 ++++ .../newsletter/newsletter.component.scss | 10 +++ .../newsletter/newsletter.component.spec.ts | 85 +++++++++++++++++++ .../newsletter/newsletter.component.ts | 64 ++++++++++++++ .../newsletter/newsletter.service.ts | 37 ++++++++ 8 files changed, 231 insertions(+), 1 deletion(-) create mode 100644 projects/sds/src/app/component-page/component-pages/newsletter/newsletter-token.model.ts create mode 100644 projects/sds/src/app/component-page/component-pages/newsletter/newsletter.component.html create mode 100644 projects/sds/src/app/component-page/component-pages/newsletter/newsletter.component.scss create mode 100644 projects/sds/src/app/component-page/component-pages/newsletter/newsletter.component.spec.ts create mode 100644 projects/sds/src/app/component-page/component-pages/newsletter/newsletter.component.ts create mode 100644 projects/sds/src/app/component-page/component-pages/newsletter/newsletter.service.ts diff --git a/projects/sds/src/app.routes.ts b/projects/sds/src/app.routes.ts index 378116ae9..59ced4e14 100644 --- a/projects/sds/src/app.routes.ts +++ b/projects/sds/src/app.routes.ts @@ -3,6 +3,10 @@ import {URL_CONST} from './app/shared/url/url.const'; export const APP_ROUTES: Routes = [ {path: '', redirectTo: 'introductions/welcome', pathMatch: 'full'}, + { + path: `introductions/newsletter`, + loadChildren: () => import('./app/component-page/component-pages.routes') + }, { path: `introductions/:${URL_CONST.urlParams.selectedSlug}`, loadChildren: () => import('./app/text-page/text-pages.routes') diff --git a/projects/sds/src/app/component-page/component-page.mapper.ts b/projects/sds/src/app/component-page/component-page.mapper.ts index 91fa307fd..34997b196 100644 --- a/projects/sds/src/app/component-page/component-page.mapper.ts +++ b/projects/sds/src/app/component-page/component-page.mapper.ts @@ -1,7 +1,13 @@ import {ComponentPage} from './component-page'; +import {NewsletterComponent} from './component-pages/newsletter/newsletter.component'; export class ComponentPageMapper { - private static readonly components: Record = {}; + private static readonly components: Record = { + newsletter: { + title: 'Newsletter', + component: NewsletterComponent + } + }; static getComponentPageComponent(name: string): ComponentPage | undefined { return this.components[name]; diff --git a/projects/sds/src/app/component-page/component-pages/newsletter/newsletter-token.model.ts b/projects/sds/src/app/component-page/component-pages/newsletter/newsletter-token.model.ts new file mode 100644 index 000000000..d8e240a1a --- /dev/null +++ b/projects/sds/src/app/component-page/component-pages/newsletter/newsletter-token.model.ts @@ -0,0 +1,8 @@ +export interface NewsletterTokenComplete { + data: NewsletterToken; +} + +export interface NewsletterToken { + id: string; + token: string; +} diff --git a/projects/sds/src/app/component-page/component-pages/newsletter/newsletter.component.html b/projects/sds/src/app/component-page/component-pages/newsletter/newsletter.component.html new file mode 100644 index 000000000..5b9f67f35 --- /dev/null +++ b/projects/sds/src/app/component-page/component-pages/newsletter/newsletter.component.html @@ -0,0 +1,16 @@ +

+ Stay informed and never miss out on new Oblique releases!
+ Get updates directly to your inbox. +

+
+ + Your Email + + + +
+ + +
+
+ diff --git a/projects/sds/src/app/component-page/component-pages/newsletter/newsletter.component.scss b/projects/sds/src/app/component-page/component-pages/newsletter/newsletter.component.scss new file mode 100644 index 000000000..03426d141 --- /dev/null +++ b/projects/sds/src/app/component-page/component-pages/newsletter/newsletter.component.scss @@ -0,0 +1,10 @@ +@use "@oblique/oblique/src/styles/scss/core/variables"; + +:host { + padding: 0.75rem; +} + +.actions { + display: flex; + column-gap: variables.$ob-spacing-default; +} diff --git a/projects/sds/src/app/component-page/component-pages/newsletter/newsletter.component.spec.ts b/projects/sds/src/app/component-page/component-pages/newsletter/newsletter.component.spec.ts new file mode 100644 index 000000000..00dcb583d --- /dev/null +++ b/projects/sds/src/app/component-page/component-pages/newsletter/newsletter.component.spec.ts @@ -0,0 +1,85 @@ +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {FormGroup, ReactiveFormsModule} from '@angular/forms'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {RouterTestingModule} from '@angular/router/testing'; +import {TranslateService} from '@ngx-translate/core'; +import {ObMockTranslateService} from '@oblique/oblique'; +import {NewsletterComponent} from './newsletter.component'; + +describe(NewsletterComponent.name, () => { + let component: NewsletterComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, RouterTestingModule, NewsletterComponent, ReactiveFormsModule, BrowserAnimationsModule], + providers: [{provide: TranslateService, useClass: ObMockTranslateService}] + }).compileComponents(); + + fixture = TestBed.createComponent(NewsletterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('property formGroup', () => { + test('that it exists', () => { + expect(component.formGroup).toBeDefined(); + }); + + test('that it is a FormGroup', () => { + expect(component.formGroup instanceof FormGroup).toBe(true); + }); + + test('that it has 1 control', () => { + expect(Object.keys(component.formGroup.controls).length).toBe(1); + }); + + test('that formControl email is initialized', () => { + expect(component.formGroup.controls.email.value).toBe(''); + }); + }); + + describe('formControl email validation', () => { + test('empty string is invalid', () => { + component.formGroup.controls.email.patchValue(''); + expect(component.formGroup.controls.email.valid).toBe(false); + }); + + test('max.musterbit.admin.ch without @ is invalid', () => { + component.formGroup.controls.email.patchValue('max.musterbit.admin.ch'); + expect(component.formGroup.controls.email.valid).toBe(false); + }); + + test('max.muster@bit.admin.ch is valid', () => { + component.formGroup.controls.email.patchValue('max.muster@bit.admin.ch'); + expect(component.formGroup.controls.email.valid).toBe(true); + }); + + test('@bit.admin.ch is invalid', () => { + component.formGroup.controls.email.patchValue('@bit.admin.ch'); + expect(component.formGroup.controls.email.valid).toBe(false); + }); + }); + + describe('handleRequest()', () => { + describe.each([ + {unsubscribe: false, email: 'max.muster@bit.admin.ch', successMessage: 'You have successfully subscribed to our newsletter!'}, + {unsubscribe: true, email: 'max.muster@bit.admin.chREMOVE', successMessage: 'You have successfully unsubscribed to our newsletter!'} + ])('with unsubscribe: $unsubscribe', ({unsubscribe, email, successMessage}) => { + beforeEach(() => { + jest.spyOn(component, 'sendRequest'); + component.formGroup.controls.email.patchValue('max.muster@bit.admin.ch'); + }); + + test(`calls sendRequest() with email: ${email} and successMessage: ${successMessage}`, () => { + component.handleRequest(unsubscribe); + expect(component.sendRequest).toHaveBeenCalledWith(email, successMessage); + }); + }); + }); +}); diff --git a/projects/sds/src/app/component-page/component-pages/newsletter/newsletter.component.ts b/projects/sds/src/app/component-page/component-pages/newsletter/newsletter.component.ts new file mode 100644 index 000000000..a73e9235f --- /dev/null +++ b/projects/sds/src/app/component-page/component-pages/newsletter/newsletter.component.ts @@ -0,0 +1,64 @@ +import {CommonModule} from '@angular/common'; +import {HttpErrorResponse} from '@angular/common/http'; +import {Component, OnInit, inject} from '@angular/core'; +import {FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms'; +import {MatButtonModule} from '@angular/material/button'; +import {MatFormFieldModule} from '@angular/material/form-field'; +import {MatInputModule} from '@angular/material/input'; +import {ObButtonModule, ObErrorMessagesModule, ObFormFieldModule, ObNotificationModule, ObNotificationService} from '@oblique/oblique'; +import {mergeMap, tap} from 'rxjs'; +import {NewsletterService} from './newsletter.service'; + +@Component({ + selector: 'app-newsletter', + templateUrl: './newsletter.component.html', + styleUrls: ['./newsletter.component.scss'], + standalone: true, + imports: [ + CommonModule, + MatButtonModule, + MatFormFieldModule, + MatInputModule, + ReactiveFormsModule, + ObFormFieldModule, + ObErrorMessagesModule, + ObButtonModule, + ObNotificationModule + ] +}) +export class NewsletterComponent implements OnInit { + formGroup: FormGroup<{email: FormControl}>; + private readonly formBuilder = inject(FormBuilder); + private readonly newsletterService = inject(NewsletterService); + private readonly obNotificationService = inject(ObNotificationService); + + ngOnInit(): void { + this.formGroup = this.formBuilder.group({ + email: ['', [Validators.email, Validators.required]] + }); + } + + handleRequest(unsubscribe: boolean): void { + let email: string = this.formGroup.get('email').value; + let successMessage = 'You have successfully subscribed to our newsletter!'; + if (unsubscribe) { + email += 'REMOVE'; + successMessage = 'You have successfully unsubscribed to our newsletter!'; + } + this.sendRequest(email, successMessage); + } + + sendRequest(email: string, successMessage: string): void { + this.newsletterService + .getNewsletterToken() + .pipe( + tap(result => (this.newsletterService.token = result.data.token)), + mergeMap(() => this.newsletterService.addNewsletterEntry(email)) + ) + .subscribe({ + complete: () => this.obNotificationService.success({title: 'Success', message: successMessage}), + error: (error: HttpErrorResponse) => + this.obNotificationService.error({title: 'Error', message: `Something went wrong!. Error: ${error?.message}`}) + }); + } +} diff --git a/projects/sds/src/app/component-page/component-pages/newsletter/newsletter.service.ts b/projects/sds/src/app/component-page/component-pages/newsletter/newsletter.service.ts new file mode 100644 index 000000000..a0a53295f --- /dev/null +++ b/projects/sds/src/app/component-page/component-pages/newsletter/newsletter.service.ts @@ -0,0 +1,37 @@ +import {HttpClient, HttpHeaders} from '@angular/common/http'; +import {Injectable, inject} from '@angular/core'; +import {Observable} from 'rxjs'; +import {NewsletterTokenComplete} from './newsletter-token.model'; + +@Injectable({ + providedIn: 'root' +}) +export class NewsletterService { + readonly baseUrl = 'https://oblique.directus.app/'; + + private bearerToken: string; + + public get token(): string { + return this.bearerToken; + } + + public set token(newToken: string) { + this.bearerToken = newToken; + } + + private readonly httpClient = inject(HttpClient); + + getNewsletterToken(): Observable { + return this.httpClient.get(`${this.baseUrl}items/newsletter_token/1`); + } + + addNewsletterEntry(email: string): Observable { + const header = this.createHeaderWithBearerToken(this.bearerToken); + return this.httpClient.post(`${this.baseUrl}items/newsletter`, {email}, {headers: header}); + } + + createHeaderWithBearerToken(token: string): HttpHeaders { + // eslint-disable-next-line @typescript-eslint/naming-convention + return new HttpHeaders({Authorization: `Bearer ${token}`}); + } +} From 8a68b3b8aa706a6b144f2333821514e7f4bb4f93 Mon Sep 17 00:00:00 2001 From: Nicole Widmer Date: Thu, 10 Oct 2024 10:06:30 +0200 Subject: [PATCH 10/50] feat(cli/toolchain): add `no-magic-numbers` rule OUI-3243 --- projects/cli/.eslintrc.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/projects/cli/.eslintrc.yml b/projects/cli/.eslintrc.yml index 25695d90f..7ab0859c5 100644 --- a/projects/cli/.eslintrc.yml +++ b/projects/cli/.eslintrc.yml @@ -16,3 +16,9 @@ overrides: - error - time - timeEnd + "@typescript-eslint/no-magic-numbers": + - error + - ignore: + - -1 + - 0 + - 1 From 2fa2f50c9b11ea8bd12f3986785ba8768a721b5b Mon Sep 17 00:00:00 2001 From: Nicole Widmer Date: Tue, 26 Nov 2024 11:53:19 +0100 Subject: [PATCH 11/50] feat(cli/toolchain): disable `no-magic-number` and `no-unsafe-assignment` rules for tests OUI-3243 --- projects/cli/.eslintrc.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/projects/cli/.eslintrc.yml b/projects/cli/.eslintrc.yml index 7ab0859c5..3521370d0 100644 --- a/projects/cli/.eslintrc.yml +++ b/projects/cli/.eslintrc.yml @@ -22,3 +22,11 @@ overrides: - -1 - 0 - 1 + - files: + - "*.spec.ts" + parserOptions: + project: + - projects/cli/tsconfig.spec.json + rules: + "@typescript-eslint/no-magic-numbers": "off" + "@typescript-eslint/no-unsafe-assignment": "off" From e3150ec8a707760c09c913789ca45ff8be954607 Mon Sep 17 00:00:00 2001 From: Nicole Widmer Date: Tue, 26 Nov 2024 11:02:58 +0100 Subject: [PATCH 12/50] feat(cli/toolchain): add `utils` scope OUI-3243 --- projects/cli/CONTRIBUTING.md | 1 + 1 file changed, 1 insertion(+) diff --git a/projects/cli/CONTRIBUTING.md b/projects/cli/CONTRIBUTING.md index a02e06ece..0b11d13c7 100644 --- a/projects/cli/CONTRIBUTING.md +++ b/projects/cli/CONTRIBUTING.md @@ -7,3 +7,4 @@ All commits related to the CLI package must use the **cli** package and 1 of the ## Scope - **toolchain** +- **utils** From 7982460290d0114239adf6ec6821906ba8b8c039 Mon Sep 17 00:00:00 2001 From: Nicole Widmer Date: Mon, 14 Oct 2024 11:38:24 +0200 Subject: [PATCH 13/50] refactor(cli/utils): encapsulate version and help options under 'ob' key in `optionDescriptions` OUI-3243 --- projects/cli/src/index.ts | 8 ++++---- projects/cli/src/utils/cli-utils.spec.ts | 12 ++++++------ projects/cli/src/utils/cli-utils.ts | 20 +++++++++++--------- 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/projects/cli/src/index.ts b/projects/cli/src/index.ts index f82c66ab3..902228a7d 100644 --- a/projects/cli/src/index.ts +++ b/projects/cli/src/index.ts @@ -7,15 +7,15 @@ import {exampleUsageText, obTitle, obUsageText, optionDescriptions, runObCommand program .name('ob') .description(cliPackage.description) - .version(cliPackage.version, optionDescriptions.version.flags, optionDescriptions.version.description) - .helpOption(optionDescriptions.help.flags, optionDescriptions.help.description) + .version(cliPackage.version, optionDescriptions.ob.version.flags, optionDescriptions.ob.version.description) + .helpOption(optionDescriptions.ob.help.flags, optionDescriptions.ob.help.description) .usage(obUsageText) .addHelpText('beforeAll', titleText(`How to use the ${obTitle}`.toUpperCase(), '')) .addHelpText( 'after', exampleUsageText([ - {command: optionDescriptions.version.command, description: optionDescriptions.version.description}, - {command: optionDescriptions.help.command, description: optionDescriptions.help.description} + {command: optionDescriptions.ob.version.command, description: optionDescriptions.ob.version.description}, + {command: optionDescriptions.ob.help.command, description: optionDescriptions.ob.help.description} ]) ) .action(handleAction) diff --git a/projects/cli/src/utils/cli-utils.spec.ts b/projects/cli/src/utils/cli-utils.spec.ts index de7771688..9da73cf6a 100644 --- a/projects/cli/src/utils/cli-utils.spec.ts +++ b/projects/cli/src/utils/cli-utils.spec.ts @@ -12,15 +12,15 @@ import { import SpyInstance = jest.SpyInstance; test('cliOptions.version.flags should be correct', () => { - expect(cliOptions.version.flags).toBe('-v, --version'); + expect(cliOptions.ob.version.flags).toBe('-v, --version'); }); test('cliOptions.version.description should be correct', () => { - expect(cliOptions.version.description).toBe('Shows the current version of @oblique/cli'); + expect(cliOptions.ob.version.description).toBe('Shows the current version of @oblique/cli'); }); test('cliOptions.version.command should be correct', () => { - expect(cliOptions.version.command).toBe('ob -v'); + expect(cliOptions.ob.version.command).toBe('ob -v'); }); test('obTitle should have correct value', () => { @@ -28,15 +28,15 @@ test('obTitle should have correct value', () => { }); test('cliOptions.help.flags should be correct', () => { - expect(cliOptions.help.flags).toBe('-h, --help'); + expect(cliOptions.ob.help.flags).toBe('-h, --help'); }); test('cliOptions.help.description should be correct', () => { - expect(cliOptions.help.description).toBe('Shows a help message for the "ob" command in the console.'); + expect(cliOptions.ob.help.description).toBe('Shows a help message for the "ob" command in the console.'); }); test('cliOptions.help.command should be correct', () => { - expect(cliOptions.help.command).toBe('ob -h'); + expect(cliOptions.ob.help.command).toBe('ob -h'); }); test('obUsageText should generate correct command usage text', () => { diff --git a/projects/cli/src/utils/cli-utils.ts b/projects/cli/src/utils/cli-utils.ts index 0e34fd93e..21784c8e1 100644 --- a/projects/cli/src/utils/cli-utils.ts +++ b/projects/cli/src/utils/cli-utils.ts @@ -1,15 +1,17 @@ #!/usr/bin/env node export const optionDescriptions = { - version: { - flags: '-v, --version', - description: 'Shows the current version of @oblique/cli', - command: 'ob -v' - }, - help: { - flags: '-h, --help', - description: getHelpText('ob'), - command: 'ob -h' + ob: { + version: { + flags: '-v, --version', + description: 'Shows the current version of @oblique/cli', + command: 'ob -v' + }, + help: { + flags: '-h, --help', + description: getHelpText('ob'), + command: 'ob -h' + } } }; export const obUsageText: string = commandUsageText(); From 56079f955f81997027df4589d04d7ddc853f5d98 Mon Sep 17 00:00:00 2001 From: Nicole Widmer Date: Mon, 14 Oct 2024 11:49:22 +0200 Subject: [PATCH 14/50] refactor(cli/utils): show information about `--help` option after error OUI-3243 --- projects/cli/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/cli/src/index.ts b/projects/cli/src/index.ts index 902228a7d..b2afc7684 100644 --- a/projects/cli/src/index.ts +++ b/projects/cli/src/index.ts @@ -20,7 +20,7 @@ program ) .action(handleAction) .showSuggestionAfterError(true) - .showHelpAfterError(true); + .showHelpAfterError('(Add --help for additional information)'); program.parse(); From 113b68c385ab3e585203161437b834f068db5b9c Mon Sep 17 00:00:00 2001 From: Nicole Widmer Date: Mon, 14 Oct 2024 12:15:23 +0200 Subject: [PATCH 15/50] refactor(cli/utils): enhance help text with usage examples and spacing adjustments OUI-3243 --- projects/cli/src/index.ts | 27 ++++++++++++-------- projects/cli/src/utils/cli-utils.spec.ts | 9 +++---- projects/cli/src/utils/cli-utils.ts | 32 ++++++++++++++++++------ 3 files changed, 44 insertions(+), 24 deletions(-) diff --git a/projects/cli/src/index.ts b/projects/cli/src/index.ts index b2afc7684..ed363eac2 100644 --- a/projects/cli/src/index.ts +++ b/projects/cli/src/index.ts @@ -2,28 +2,35 @@ import {program} from '@commander-js/extra-typings'; import * as cliPackage from '../package.json'; -import {exampleUsageText, obTitle, obUsageText, optionDescriptions, runObCommand, startObCommand, titleText} from './utils/cli-utils'; +import { + commandUsageText, + createAdditionalHelpText, + obExamples, + obTitle, + optionDescriptions, + runObCommand, + startObCommand, + titleText +} from './utils/cli-utils'; program .name('ob') .description(cliPackage.description) .version(cliPackage.version, optionDescriptions.ob.version.flags, optionDescriptions.ob.version.description) .helpOption(optionDescriptions.ob.help.flags, optionDescriptions.ob.help.description) - .usage(obUsageText) + .usage(commandUsageText('')) .addHelpText('beforeAll', titleText(`How to use the ${obTitle}`.toUpperCase(), '')) - .addHelpText( - 'after', - exampleUsageText([ - {command: optionDescriptions.ob.version.command, description: optionDescriptions.ob.version.description}, - {command: optionDescriptions.ob.help.command, description: optionDescriptions.ob.help.description} - ]) - ) + .addHelpText('after', createAdditionalHelpText('\nExample usages:\n', obExamples, getMaxCommandLength(obExamples))) .action(handleAction) .showSuggestionAfterError(true) .showHelpAfterError('(Add --help for additional information)'); program.parse(); -function handleAction(options: Record): void { +export function handleAction(options: Record): void { startObCommand(options, runObCommand, 'Oblique CLI completed in'); } + +export function getMaxCommandLength(examples: {command: string; description: string}[]): number { + return examples.map(entry => entry.command.length).reduce((max, length) => (length > max ? length : max), 0); +} diff --git a/projects/cli/src/utils/cli-utils.spec.ts b/projects/cli/src/utils/cli-utils.spec.ts index 9da73cf6a..7dca399f4 100644 --- a/projects/cli/src/utils/cli-utils.spec.ts +++ b/projects/cli/src/utils/cli-utils.spec.ts @@ -4,7 +4,6 @@ import { exampleUsageText, getHelpText, obTitle, - obUsageText, runObCommand, startObCommand, titleText @@ -39,14 +38,12 @@ test('cliOptions.help.command should be correct', () => { expect(cliOptions.ob.help.command).toBe('ob -h'); }); -test('obUsageText should generate correct command usage text', () => { - expect(obUsageText).toBe(' [option]'); -}); - test('runObCommand should log correct message', () => { const consoleSpy = jest.spyOn(console, 'info').mockImplementation(() => {}); runObCommand(); - expect(consoleSpy).toHaveBeenCalledWith('\nOblique CLI is running now!\n'); + expect(consoleSpy).toHaveBeenCalledWith( + '\n Use `ob new ` to create a new project\n Use `ob --help` to explore the available commands\n' + ); consoleSpy.mockRestore(); }); diff --git a/projects/cli/src/utils/cli-utils.ts b/projects/cli/src/utils/cli-utils.ts index 21784c8e1..1bef77231 100644 --- a/projects/cli/src/utils/cli-utils.ts +++ b/projects/cli/src/utils/cli-utils.ts @@ -14,16 +14,23 @@ export const optionDescriptions = { } } }; -export const obUsageText: string = commandUsageText(); + +export const obExamples = [ + {command: optionDescriptions.ob.version.command, description: optionDescriptions.ob.version.description}, + {command: optionDescriptions.ob.help.command, description: optionDescriptions.ob.help.description} +]; + +const spaceUnit = ` `; +const projectNamePlaceholder = ``; export const runObCommand = (): void => { - console.info('\nOblique CLI is running now!\n'); + console.info( + `\n${spaceUnit}Use \`ob new ${projectNamePlaceholder}\` to create a new project\n` + + `${spaceUnit}Use \`ob --help\` to explore the available commands\n` + ); }; export const obTitle = `Oblique Cli`; -const spaceUnit = ` `; -const tabulatorAmount = 2; -const defaultTabulatorSpace: string = `\t`.repeat(tabulatorAmount); export function getHelpText(command: 'ob'): string { return `Shows a help message for the "${command}" command in the console.`; @@ -46,9 +53,18 @@ export function commandUsageText(subCommand: '' | 'new' | 'update' = '< export function exampleUsageText(examples: {command: string; description: string}[]): string { const title = '\nExamples of use:\n'; - return [title, examples.map(example => `${spaceUnit}${example.command}${defaultTabulatorSpace}${example.description}`).join('\n')].join( - '' - ); + return [title, examples.map(example => `${spaceUnit}${example.command} ${example.description}`).join('\n')].join(''); +} +const PADDING_SIZE = 5; +export function createAdditionalHelpText( + title: string, + examples: {command: string; description: string}[], + maxCommandWidth: number +): string { + return [ + title, + examples.map(example => `${spaceUnit}${example.command.padEnd(maxCommandWidth + PADDING_SIZE, ' ')}${example.description}`).join('\n') + ].join(''); } export function titleText(title: string, delimiterStart = '\n', delimiterEnd = '\n'): string { From 42ea199ec2b6dd5829a04234834de2c8bf31fd0b Mon Sep 17 00:00:00 2001 From: Nicole Widmer Date: Tue, 15 Oct 2024 15:52:31 +0200 Subject: [PATCH 16/50] fix(cli/utils): correct example command output formatting OUI-3243 --- projects/cli/src/utils/cli-utils.spec.ts | 77 +++++++++++++++++++++++- projects/cli/src/utils/cli-utils.ts | 2 +- 2 files changed, 76 insertions(+), 3 deletions(-) diff --git a/projects/cli/src/utils/cli-utils.spec.ts b/projects/cli/src/utils/cli-utils.spec.ts index 7dca399f4..6a5d7d18d 100644 --- a/projects/cli/src/utils/cli-utils.spec.ts +++ b/projects/cli/src/utils/cli-utils.spec.ts @@ -1,6 +1,7 @@ import { optionDescriptions as cliOptions, commandUsageText, + createAdditionalHelpText, exampleUsageText, getHelpText, obTitle, @@ -136,9 +137,81 @@ test('exampleUsageText should return formatted examples', () => { {command: 'ob new', description: 'Creates a new project'}, {command: 'ob update', description: 'Updates the project'} ]; - const expectedOutput = `Examples of use: ob new Creates a new project ob update Updates the project`; + const expectedOutput = ` +Examples of use: + ob newCreates a new project + ob updateUpdates the project`; const result = exampleUsageText(examples); - expect(cleanOutput(result)).toBe(expectedOutput); + expect(result).toBe(expectedOutput); +}); + +describe('createAdditionalHelpText', () => { + const spaceUnit = ' '; + + test('should return a properly formatted string', () => { + const title = 'Usage'; + const examples = [ + {command: 'cmd1', description: 'description1'}, + {command: 'cmd2', description: 'description2'} + ]; + const maxCommandWidth = 4; + + const result = createAdditionalHelpText(title, examples, maxCommandWidth); + const expected = ['Usage', ` ${spaceUnit}cmd1 description1\n${spaceUnit} cmd2 description2`].join(''); + + expect(result).toBe(expected); + }); + + test('should handle empty examples array', () => { + const title = 'Usage'; + const examples: {command: string; description: string}[] = []; + const maxCommandWidth = 4; + + const result = createAdditionalHelpText(title, examples, maxCommandWidth); + const expected = 'Usage'; + + expect(result).toBe(expected); + }); + + test('should pad commands correctly with given maxCommandWidth', () => { + const title = 'Usage'; + const examples = [ + {command: 'cmd1', description: 'description1'}, + {command: 'cmd2', description: 'description2'} + ]; + const maxCommandWidth = 5; + + const result = createAdditionalHelpText(title, examples, maxCommandWidth); + const expected = ['Usage', `${spaceUnit}cmd1 description1\n${spaceUnit}cmd2 description2`].join(''); + + expect(cleanOutput(result)).toBe(cleanOutput(expected)); + }); + + test('should handle longer commands correctly without truncation', () => { + const title = 'Usage'; + const examples = [ + {command: 'longcommand1 ', description: 'description1'}, + {command: 'cmd2', description: 'description2'} + ]; + + const result = createAdditionalHelpText(title, examples, examples[0].command.length); + + expect(result).toBe( + [ + [ + title, + ' ', + examples[0].command, + ' ', + examples[0].description, + '\n ', + examples[1].command, + ' ', + examples[1].description + ].join('') + ].join('') + ); + }); }); function cleanOutput(output: Buffer | string): string { diff --git a/projects/cli/src/utils/cli-utils.ts b/projects/cli/src/utils/cli-utils.ts index 1bef77231..2d3a27130 100644 --- a/projects/cli/src/utils/cli-utils.ts +++ b/projects/cli/src/utils/cli-utils.ts @@ -53,7 +53,7 @@ export function commandUsageText(subCommand: '' | 'new' | 'update' = '< export function exampleUsageText(examples: {command: string; description: string}[]): string { const title = '\nExamples of use:\n'; - return [title, examples.map(example => `${spaceUnit}${example.command} ${example.description}`).join('\n')].join(''); + return [title, examples.map(example => `${spaceUnit}${example.command}${example.description}`).join('\n')].join(''); } const PADDING_SIZE = 5; export function createAdditionalHelpText( From 407c86c2991788fbba3358f96c005bd10871b1e7 Mon Sep 17 00:00:00 2001 From: Nicole Widmer Date: Tue, 26 Nov 2024 11:11:25 +0100 Subject: [PATCH 17/50] feat(cli/toolchain): add `new` scope OUI-3243 --- projects/cli/CONTRIBUTING.md | 1 + 1 file changed, 1 insertion(+) diff --git a/projects/cli/CONTRIBUTING.md b/projects/cli/CONTRIBUTING.md index 0b11d13c7..2b757eb87 100644 --- a/projects/cli/CONTRIBUTING.md +++ b/projects/cli/CONTRIBUTING.md @@ -8,3 +8,4 @@ All commits related to the CLI package must use the **cli** package and 1 of the - **toolchain** - **utils** +- **new** From 13df8c84c92827975b83fc861c71772420378972 Mon Sep 17 00:00:00 2001 From: Nicole Widmer Date: Tue, 15 Oct 2024 15:57:58 +0200 Subject: [PATCH 18/50] refactor(cli/utils): simplify `startObCommand` signature OUI-3243 --- projects/cli/src/index.spec.ts | 4 ++-- projects/cli/src/index.ts | 2 +- projects/cli/src/utils/cli-utils.spec.ts | 10 +++++----- projects/cli/src/utils/cli-utils.ts | 8 ++------ 4 files changed, 10 insertions(+), 14 deletions(-) diff --git a/projects/cli/src/index.spec.ts b/projects/cli/src/index.spec.ts index 5300307bb..3703ceb97 100644 --- a/projects/cli/src/index.spec.ts +++ b/projects/cli/src/index.spec.ts @@ -30,8 +30,8 @@ describe('Oblique CLI', () => { describe.each(['-v', '--version'])('version option with %s', flag => { test(`stdout should contain the version from package.json`, () => { - const result = spawnSync('ts-node', [cliPath, flag], options); - expect(cleanOutput(result.stdout.toString())).toBe(cleanOutput(packageFile.version)); + const result = spawnSync('ts-node', [cliPath, flag]); + expect(cleanOutput(result.stdout)).toBe(cleanOutput(packageFile.version)); }); test(`stderr should be empty`, () => { diff --git a/projects/cli/src/index.ts b/projects/cli/src/index.ts index ed363eac2..93aa4cc05 100644 --- a/projects/cli/src/index.ts +++ b/projects/cli/src/index.ts @@ -28,7 +28,7 @@ program program.parse(); export function handleAction(options: Record): void { - startObCommand(options, runObCommand, 'Oblique CLI completed in'); + startObCommand(runObCommand, 'Oblique CLI completed in', options); } export function getMaxCommandLength(examples: {command: string; description: string}[]): number { diff --git a/projects/cli/src/utils/cli-utils.spec.ts b/projects/cli/src/utils/cli-utils.spec.ts index 6a5d7d18d..49900baa4 100644 --- a/projects/cli/src/utils/cli-utils.spec.ts +++ b/projects/cli/src/utils/cli-utils.spec.ts @@ -85,7 +85,7 @@ describe('startObCommand', () => { const options = {key: 'value'}; const label = 'Test Label'; - startObCommand(options, callback, label); + startObCommand(callback, label, options); expect(callback).toHaveBeenCalledWith(options); }); @@ -94,7 +94,7 @@ describe('startObCommand', () => { const options = {key: 'value'}; const label = 'Test Label'; - startObCommand(options, callback, label); + startObCommand(callback, label, options); expect(consoleTimeSpy).toHaveBeenCalledWith(label); }); @@ -103,7 +103,7 @@ describe('startObCommand', () => { const options = {key: 'value'}; const label = 'Test Label'; - startObCommand(options, callback, label); + startObCommand(callback, label, options); expect(consoleTimeEndSpy).toHaveBeenCalledWith(label); }); @@ -114,9 +114,9 @@ describe('startObCommand', () => { const options = {key: 'value'}; const label = 'Test Label'; - startObCommand(options, callback, label); + startObCommand(callback, label, options); - expect(consoleInfoSpy).toHaveBeenCalledWith(`${uppercaseTitle} `); + expect(consoleInfoSpy).toHaveBeenCalledWith(uppercaseTitle); }); }); diff --git a/projects/cli/src/utils/cli-utils.ts b/projects/cli/src/utils/cli-utils.ts index 2d3a27130..df6698978 100644 --- a/projects/cli/src/utils/cli-utils.ts +++ b/projects/cli/src/utils/cli-utils.ts @@ -36,12 +36,8 @@ export function getHelpText(command: 'ob'): string { return `Shows a help message for the "${command}" command in the console.`; } -export const startObCommand = ( - options: Record, - callback: (options: Record) => void, - label: string -): void => { - console.info(titleText(obTitle.toUpperCase(), '', ' ')); +export const startObCommand = (callback: (options: T) => void, label: string, options: T): void => { + console.info(obTitle.toUpperCase()); console.time(label); callback(options); console.timeEnd(label); From b2ddd17bfa701c01058298e39b74862b4424d6e7 Mon Sep 17 00:00:00 2001 From: Nicole Widmer Date: Tue, 15 Oct 2024 15:47:03 +0200 Subject: [PATCH 19/50] feat(cli/new): add `ob new` command OUI-3243 --- projects/cli/src/index.spec.ts | 92 +++-- projects/cli/src/index.ts | 5 + projects/cli/src/new/ob-new.model.ts | 67 ++++ projects/cli/src/new/ob-new.spec.ts | 319 +++++++++++++++ projects/cli/src/new/ob-new.ts | 149 +++++++ projects/cli/src/new/schema.json | 134 +++++++ projects/cli/src/utils/cli-utils.spec.ts | 379 +++++++++--------- projects/cli/src/utils/cli-utils.ts | 43 +- projects/cli/src/utils/ob-cli.model.ts | 3 + .../src/utils/ob-configure-command.spec.ts | 165 ++++++++ .../cli/src/utils/ob-configure-command.ts | 72 ++++ 11 files changed, 1194 insertions(+), 234 deletions(-) create mode 100644 projects/cli/src/new/ob-new.model.ts create mode 100644 projects/cli/src/new/ob-new.spec.ts create mode 100644 projects/cli/src/new/ob-new.ts create mode 100644 projects/cli/src/new/schema.json create mode 100644 projects/cli/src/utils/ob-cli.model.ts create mode 100644 projects/cli/src/utils/ob-configure-command.spec.ts create mode 100644 projects/cli/src/utils/ob-configure-command.ts diff --git a/projects/cli/src/index.spec.ts b/projects/cli/src/index.spec.ts index 3703ceb97..03beba53e 100644 --- a/projects/cli/src/index.spec.ts +++ b/projects/cli/src/index.spec.ts @@ -1,9 +1,9 @@ -import {SpawnSyncOptions, spawnSync} from 'child_process'; -import * as path from 'path'; -import * as packageFile from './../package.json'; +import {SpawnSyncOptions, spawnSync} from 'node:child_process'; +import path from 'path'; +import * as cliPackage from '../package.json'; -describe('Oblique CLI', () => { - let cliPath: string; +describe('index.ts', () => { + const cliPath = path.resolve(__dirname, './index.ts'); const workingDirectory = path.resolve(__dirname, '../../../../'); const options = { encoding: 'utf-8', @@ -12,58 +12,56 @@ describe('Oblique CLI', () => { cwd: workingDirectory } as SpawnSyncOptions; - beforeAll(() => { - cliPath = path.resolve(__dirname, './index.ts'); - }); + describe('Oblique CLI with spawnSync', () => { + describe.each(['-h', '--help'])('help option with %s', flag => { + test(`stdout should not be empty`, () => { + const result = spawnSync('ts-node', [cliPath, flag], options); + expect(result.stdout).not.toBe(''); + }); - describe.each(['-h', '--help'])('help option with %s', flag => { - test(`stdout should not be empty`, () => { - const result = spawnSync('ts-node', [cliPath, flag], options); - expect(result.stdout).not.toBe(''); + test(`stderr should be empty`, () => { + const result = spawnSync('ts-node', [cliPath, flag], options); + expect(result.stderr).toBe(''); + }); }); - test(`stderr should be empty`, () => { - const result = spawnSync('ts-node', [cliPath, flag], options); - expect(result.stderr).toBe(''); - }); - }); + describe.each(['-v', '--version'])('version option with %s', flag => { + test(`stdout should contain the version from package.json`, () => { + const result = spawnSync('ts-node', [cliPath, flag]); + expect(cleanOutput(result.stdout)).toBe(cleanOutput(cliPackage.version)); + }); - describe.each(['-v', '--version'])('version option with %s', flag => { - test(`stdout should contain the version from package.json`, () => { - const result = spawnSync('ts-node', [cliPath, flag]); - expect(cleanOutput(result.stdout)).toBe(cleanOutput(packageFile.version)); + test(`stderr should be empty`, () => { + const result = spawnSync('ts-node', [cliPath, flag], options); + expect(result.stderr).toBe(''); + }); }); - test(`stderr should be empty`, () => { - const result = spawnSync('ts-node', [cliPath, flag], options); - expect(result.stderr).toBe(''); - }); - }); + describe.each([ + {correctOption: '--help', wrongOption: '--holp'}, + {correctOption: '--version', wrongOption: '--vorsion'} + ])(`Wrong Option $wrongOption instead of $correctOption`, ({wrongOption, correctOption}) => { + test(`show suggestion "(Did you mean ${correctOption}?)"`, () => { + const result = spawnSync('ts-node', [cliPath, wrongOption], options); + expect(cleanOutput(result.stderr.toString())).toContain(`(Did you mean ${correctOption}?)`); + }); - describe.each([ - {correctOption: '--help', wrongOption: '--holp'}, - {correctOption: '--version', wrongOption: '--vorsion'} - ])(`Wrong Option $wrongOption instead of $correctOption`, ({wrongOption, correctOption}) => { - test(`show suggestion "(Did you mean ${correctOption}?)"`, () => { - const result = spawnSync('ts-node', [cliPath, wrongOption], options); - expect(cleanOutput(result.stderr.toString())).toContain(`(Did you mean ${correctOption}?)`); + test(`stdout should be empty`, () => { + const result = spawnSync('ts-node', [cliPath, wrongOption], options); + expect(result.stdout).toBe(''); + }); }); - test(`stdout should be empty`, () => { - const result = spawnSync('ts-node', [cliPath, wrongOption], options); - expect(result.stdout).toBe(''); - }); - }); - - describe('error handling for unknown options', () => { - test(`stderr should contain "error: unknown option '--unicornpoop'"`, () => { - const result = spawnSync('ts-node', [cliPath, '--unicornpoop'], options); - expect(cleanOutput(result.stderr)).toContain(`error: unknown option '--unicornpoop'`); - }); + describe('error handling for unknown options', () => { + test(`stderr should contain "error: unknown option '--unicornpoop'"`, () => { + const result = spawnSync('ts-node', [cliPath, '--unicornpoop'], options); + expect(cleanOutput(result.stderr)).toContain(`error: unknown option '--unicornpoop'`); + }); - test(`stdout should be empty for unknown option`, () => { - const result = spawnSync('ts-node', [cliPath, '--unicornpoop'], options); - expect(result.stdout).toBe(''); + test(`stdout should be empty for unknown option`, () => { + const result = spawnSync('ts-node', [cliPath, '--unicornpoop'], options); + expect(result.stdout).toBe(''); + }); }); }); diff --git a/projects/cli/src/index.ts b/projects/cli/src/index.ts index 93aa4cc05..c0d5e16d4 100644 --- a/projects/cli/src/index.ts +++ b/projects/cli/src/index.ts @@ -2,6 +2,7 @@ import {program} from '@commander-js/extra-typings'; import * as cliPackage from '../package.json'; +import {createObNewCommand} from './new/ob-new'; import { commandUsageText, createAdditionalHelpText, @@ -25,6 +26,10 @@ program .showSuggestionAfterError(true) .showHelpAfterError('(Add --help for additional information)'); +const obNewCommandConfigured = createObNewCommand(); + +program.addCommand(obNewCommandConfigured); + program.parse(); export function handleAction(options: Record): void { diff --git a/projects/cli/src/new/ob-new.model.ts b/projects/cli/src/new/ob-new.model.ts new file mode 100644 index 000000000..3372af0eb --- /dev/null +++ b/projects/cli/src/new/ob-new.model.ts @@ -0,0 +1,67 @@ +import {Command, OptionValues} from '@commander-js/extra-typings'; +import {getVersionedDependency, projectNamePlaceholder} from '../utils/cli-utils'; + +export type ObNewOptions = Record; + +export interface ObNewSchemaOption { + type: string; + description: string; + shortFlag?: string; + defaultValue?: boolean | string; + flagValuePlaceholder?: string; + defaultValueDescription?: string; + choices?: string[]; + mandatory?: boolean; + resources?: string[]; +} + +export interface HandleObNewActionOptions { + projectName: string; + command: Command<[string], OptionValues>; +} + +export type OptionKeys = + | 'title' + | 'locales' + | 'ajv' + | 'unknownRoute' + | 'httpInterceptors' + | 'banner' + | 'environments' + | 'externalLink' + | 'prefix' + | 'jest' + | 'protractor' + | 'npmrc' + | 'proxy' + | 'sonar' + | 'eslint' + | 'husky'; + +export type ImmutableOptionsType = 'no-standalone' | 'no-ssr' | 'style'; + +export const immutableOptions: Record = { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'no-standalone': { + description: `Oblique doesn't support standalone components` + }, + // eslint-disable-next-line @typescript-eslint/naming-convention + 'no-ssr': { + description: `Oblique doesn't support server side rendering` + }, + style: { + value: 'scss', + description: 'Oblique uses SCSS' + } +}; + +export const obNewConfig = { + obNewSummaryText: `Creates an Oblique project`, + projectNameArgument: { + description: `Unique name for your new project`, + argumentName: projectNamePlaceholder + } +}; + +export const createsWorkspaceMessage = `\nCreates a new Angular workspace`; +export const ngAddStringCommand = `npx ${getVersionedDependency('@angular/cli')} add ${getVersionedDependency('@oblique/oblique')}`; diff --git a/projects/cli/src/new/ob-new.spec.ts b/projects/cli/src/new/ob-new.spec.ts new file mode 100644 index 000000000..7e8198e95 --- /dev/null +++ b/projects/cli/src/new/ob-new.spec.ts @@ -0,0 +1,319 @@ +import {Command, OptionValues} from '@commander-js/extra-typings'; +import * as cliPackage from '../../package.json'; +import * as obNewSchema from './schema.json'; +import {execSync} from 'child_process'; +import {addStringFlag, getTitlesCommandOption} from './ob-new'; +import {HandleObNewActionOptions, ObNewOptions, ObNewSchemaOption, obNewConfig} from './ob-new.model'; +import {currentVersions} from '../utils/cli-utils'; +import path from 'path'; + +const nodeChildProcess: typeof import('node:child_process') = jest.requireActual('node:child_process'); +interface ObNewModuleType { + createAddObliqueCommand: (command: string, options: Record, projectName: string) => string; + runNgNewAngularWorkspace: (projectName?: string, prefix?: string | 'app') => void; + handleObNewActions: (options: HandleObNewActionOptions) => void; + addImmutableOptionsText: (command: Command<[string], OptionValues>) => Command<[string], OptionValues>; + createObNewCommand: () => Command<[string], OptionValues>; + runAddOblique: (options: ObNewOptions, projectName: string) => void; + addStringFlag: (option: {key: string; value: string}, projectName: string, property: ObNewSchemaOption) => string[]; + handleAction: (options: HandleObNewActionOptions) => void; + getTitlesCommandOption: (option: {key: string; value: string}, projectName: string) => string; +} + +describe('Ob new command', () => { + const projectName = 'SuperduperProject'; + // eslint-disable-next-line @typescript-eslint/no-var-requires + let obNew: ObNewModuleType = require('./ob-new') as ObNewModuleType; + let obNewCommand: Command<[string], OptionValues>; + let parsedObNewCommand: Command<[string], OptionValues>; + describe('addStringFlag', () => { + test.each([ + { + description: 'title command option when option key is "title" and property type is "string"', + option: {key: 'title', value: 'My Project'}, + property: {type: 'string'} as ObNewSchemaOption, + expected: [getTitlesCommandOption({key: 'title', value: 'My Project'}, projectName)] + }, + { + description: 'string flag command option for non-title keys when property type is "string"', + option: {key: 'description', value: 'This is a test'}, + property: {type: 'string'} as ObNewSchemaOption, + expected: ['--description="This is a test"'] + }, + { + description: 'empty array when property type is not "string"', + option: {key: 'title', value: 'My Project'}, + property: {type: 'number'} as ObNewSchemaOption, + expected: [] + }, + { + description: 'empty array when property does not have a "type" property', + option: {key: 'title', value: 'My Project'}, + property: {type: undefined} as ObNewSchemaOption, + expected: [] + } + ])('$description', ({option, property, expected}) => { + const result = addStringFlag(option, projectName, property); + expect(result).toEqual(expected); + }); + }); + + describe('getTitlesCommandOption', () => { + test.each([ + { + description: 'title option when option key is "title" and value is not empty', + option: {key: 'title', value: 'My Custom Title'}, + expected: '--title="My Custom Title"' + }, + { + description: 'title option when option key is "title" and value is an empty string', + option: {key: 'title', value: ''}, + expected: `--title="${projectName}"` + }, + { + description: 'empty string when option key is not "title"', + option: {key: 'description', value: 'A description'}, + expected: '' + } + ])('returns $description', ({option, expected}) => { + const result = getTitlesCommandOption(option, projectName); + expect(result).toBe(expected); + }); + }); + + describe('after createObNewCommand', () => { + describe('without error', () => { + beforeAll(() => { + const nodeChildProcessWithoutErrorSpy = jest.spyOn(nodeChildProcess, 'execSync').mockImplementation(() => 'ok'); + jest.mock('node:child_process', () => ({ + ...jest.requireActual('node:child_process'), + execSync: nodeChildProcessWithoutErrorSpy + })); + jest.spyOn(console, 'info'); + jest.spyOn(console, 'timeEnd'); + obNew = require('./ob-new'); + obNewCommand = obNew.createObNewCommand(); + parsedObNewCommand = obNewCommand.parse([projectName], {from: 'user'}); + }); + + describe('command setup', () => { + test('should have name "new"', () => { + expect(parsedObNewCommand.name()).toBe('new'); + }); + + test('should have description: "Creates a new Angular project and install Oblique"', () => { + expect(parsedObNewCommand.description()).toBe(obNewConfig.obNewSummaryText); + }); + + test(`should have package version ${cliPackage.version}`, () => { + expect(parsedObNewCommand.version()).toBe(cliPackage.version); + }); + + test(`should not have aliases`, () => { + expect(parsedObNewCommand.aliases()).toStrictEqual([]); + }); + + test(`should have usage`, () => { + expect(parsedObNewCommand.usage()).toStrictEqual(' [...options]'); + }); + test(`should have description`, () => { + expect(parsedObNewCommand.description()).toStrictEqual(obNewConfig.obNewSummaryText); + }); + + test(`should have summary`, () => { + expect(parsedObNewCommand.summary()).toStrictEqual(obNewConfig.obNewSummaryText); + }); + }); + + /* eslint-disable no-console */ + describe.each([ + {index: 1, message: 'OBLIQUE CLI', type: 'info'}, + {index: 2, message: '\nCreates a new Angular workspace', type: 'info'}, + {index: 3, message: '[Info]: Installs Angular Material', type: 'info'}, + {index: 4, message: '[Complete]: Oblique added', type: 'info'}, + {index: 1, message: 'Oblique CLI ob new completed in', type: 'timeEnd'} + ])('calls console ', ({index, message, type}) => { + test(`${type} ${message}`, () => { + // eslint + expect(console[type]).toHaveBeenNthCalledWith(index, message); + }); + }); + + const optionProperties = Object.entries(obNewSchema.properties).map(property => ({key: property[0], value: property[1]})); + /* eslint-enable */ + + describe.each(optionProperties)('default option', ({key, value}) => { + test(`should have option for ${key} with default value "${value.defaultValue}"`, () => { + expect(parsedObNewCommand.opts()[key]).toBe(value.defaultValue); + }); + }); + + describe('help text', () => { + const helpTextLines = [ + { + description: 'Usage information for creating a new Angular project', + expected: `Usage: new [...options] ${obNewConfig.obNewSummaryText}` + }, + { + description: 'Argument for the project name', + expected: 'Arguments: project-name Unique name for your new project' + }, + { + description: 'Section header for options', + expected: 'Options:' + }, + { + description: 'Option to output the current version of the CLI', + expected: 'Options: -v, --version Shows the current version of @oblique/cli ' + }, + { + description: "Option to specify the application's title", + expected: + "--title Add the specified application's title: The title will be visible in the header of your application. (default: project name.)" + }, + { + description: 'Option to specify supported locales', + expected: '--locales Supported locales: Use a whitespace separated list. (default: "de-CH fr-CH it-CH")' + }, + { + description: 'Option to specify environment files', + expected: + "--environments Environment files: Use a whitespace separated list or leave a whitespace to skip the feature. 'local' will create an 'environment.ts' file, all other environments will create a corresponding 'environment..ts' file. (default: local dev ref test abn prod)" + }, + { + description: 'Option to specify the prefix for components and directives', + expected: + '--prefix Prefix configuration: The prefix for components and directive\'s selectors. Leave empty for no prefix. (default: "app")' + }, + { + description: 'Option to configure a proxy server', + expected: + '--proxy Proxy configuration: Defines the port for the proxy configuration for server connection. (default: " ")' + }, + { + description: 'Option to add Ajv dependency for form validation', + expected: + '--ajv Add Ajv dependency: Value "true" enables form validation based on a schema delivered by the server. See more information at https://www.npmjs.com/package/ajv (default: true)' + }, + { + description: 'Option for unknown route management', + expected: + "--unknownRoute Unknown route management: This will display custom 404 pages instead of redirecting to the home page. See more information at Oblique's Unknown route API at https://oblique.bit.admin.ch/helpers/unknown-route/api (default: true)" + }, + { + description: 'Option to add HTTP interceptors', + expected: + "-httpInterceptors Http interceptor: If set to true, it will provide the ObHttpApiInterceptor in the app.module.ts. The interceptor displays a spinner on API calls and a notification on errors. See more information at Oblique's ObHttpInterceptor API at https://oblique.bit.admin.ch/helpers/http-interceptor/api (default: true)" + }, + { + description: 'Option to show a banner for the current environment', + expected: + "--banner Banner to show current environment: The ObBanner will show the current environment in the header. This feature is only available if at least 1 environment will be defined. To define your environments, use the option --environments . See more information at Oblique's ObBanner API at https://oblique.bit.admin.ch/helpers/banner/api" + }, + { + description: 'Option to add the external link module', + expected: + "--externalLink External link: If true, it imports the ObExternalLinkModule. This feature automatically enhances external links. See more information at Oblique's External link API at https://oblique.bit.admin.ch/components/external-link/api (default: true)" + }, + { + description: 'Option to use Jest for unit tests', + expected: + "--jest Jest for unit tests: If true, Karma/Jasmine will be replaced with Jest as your application's testing framework. See more information at Jest at npm https://www.npmjs.com/package/jest and Jest's documentation: https://jestjs.io/docs/getting-started (default: true)" + }, + { + description: 'Option to keep Protractor for end-to-end tests', + expected: '--protractor Protractor for end to end tests: If you use this flag, you keep Protractor for e2e tests.' + }, + { + description: 'Option to create an .npmrc file', + expected: + '--npmrc Create .npmrc: If you use this flag, it adds an .npmrc file, suitable for projects located within confederation/federal network. (default: true)' + }, + { + description: 'Option to add Sonar configuration', + expected: '--sonar Sonar configuration: If set to true, a Sonar configuration is added. (default: true)' + }, + { + description: 'Option to add ESLint and Prettier', + expected: + ' --eslint ESLint & Prettier: If true, it adds eslint & prettier configuration as used by the Oblique team. See more information at ESLint Documentation: https://eslint.org/docs/latest/use/getting-started (default: true)' + }, + { + description: 'Option to add Husky configuration for git hooks', + expected: + "--husky Husky configuration: If true, it adds git hooks to automatically format changed files. See more information at Husky Documentation at https://typicode.github.io/husky/ and Husky's package at npm https://www.npmjs.com/package/husky (default: true)" + }, + { + description: 'Option to display help information', + expected: '-h, --help Shows a help message for the "ob new" command in the console' + } + ]; + test.each(helpTextLines)('has $description', ({expected}) => { + expect(cleanOutput(parsedObNewCommand.helpInformation())).toContain(expected); + }); + }); + + describe('handleObNewActions execSync calls', () => { + test(`should call npx @angular/cli@${currentVersions['@angular/cli']} new ${projectName} --no-standalone --no-ssr --style scss --prefix=app`, () => { + expect(execSync).toHaveBeenNthCalledWith( + 1, + `npx @angular/cli@${currentVersions['@angular/cli']} new ${projectName} --no-standalone --no-ssr --style scss --prefix=app`, + { + cwd: process.cwd(), + stdio: 'inherit' + } + ); + }); + + test(`should call npm installl @angular/material@${currentVersions['@angular/material']}`, () => { + expect(execSync).toHaveBeenNthCalledWith(2, `npm install @angular/material@${currentVersions['@angular/material']}`, { + cwd: path.join(process.cwd(), projectName), + stdio: 'inherit' + }); + }); + + test(`should call npx ${projectName} with default parameter`, () => { + expect(execSync).toHaveBeenNthCalledWith( + 3, + `npx @angular/cli@${currentVersions['@angular/cli']} add @oblique/oblique@${currentVersions['@oblique/oblique']} --title="${projectName}" --locales="de-CH fr-CH it-CH" --environments="local dev ref test abn prod" --prefix="app" --proxy=" " --jenkins="" --ajv --unknownRoute --httpInterceptors --no-banner --externalLink --jest --no-protractor --npmrc --sonar --no-static --eslint --husky`, + { + cwd: path.join(process.cwd(), projectName), + stdio: 'inherit' + } + ); + }); + }); + }); + + describe('with error in ', () => { + const errorMessage = 'bad bad error'; + beforeAll(() => { + const nodeChildProcessWithErrorSpy = jest + .spyOn(nodeChildProcess, 'execSync') + .mockImplementationOnce(() => { + throw new Error(errorMessage); + }) + .mockImplementation(() => 'ok'); + jest.mock('node:child_process', () => ({ + ...jest.requireActual('node:child_process'), + execSync: nodeChildProcessWithErrorSpy + })); + jest.spyOn(console, 'error'); + jest.spyOn(console, 'info'); + jest.spyOn(console, 'timeEnd'); + obNew = require('./ob-new'); + obNewCommand = obNew.createObNewCommand(); + // eslint-disable @typescript-eslint/no-unsafe-call @typescript-eslint/no-unsafe-member-access + parsedObNewCommand = obNewCommand.parse([projectName], {from: 'user'}); + }); + + test(`should throw error`, () => { + expect(console.error).toHaveBeenCalledWith('Installation failed: ', Error(errorMessage)); + }); + }); + }); + + function cleanOutput(output: string): string { + return output.replace(/\s+/g, ' ').trim(); + } +}); diff --git a/projects/cli/src/new/ob-new.ts b/projects/cli/src/new/ob-new.ts new file mode 100644 index 000000000..ce6611d0f --- /dev/null +++ b/projects/cli/src/new/ob-new.ts @@ -0,0 +1,149 @@ +import {Command, OptionValues} from '@commander-js/extra-typings'; +import {execSync} from 'child_process'; +import * as cliPackage from '../../package.json'; +import {commandUsageText, getVersionedDependency, optionDescriptions, projectNamePlaceholder, startObCommand} from '../utils/cli-utils'; +import {addObNewCommandOptions, convertOptionPropertyNames} from '../utils/ob-configure-command'; +import { + HandleObNewActionOptions, + ObNewOptions, + ObNewSchemaOption, + createsWorkspaceMessage, + immutableOptions, + ngAddStringCommand, + obNewConfig +} from './ob-new.model'; +import * as obCliNewSchema from './schema.json'; + +export function createObNewCommand(): Command<[string], OptionValues> { + const command = new Command<[string], OptionValues>(); + const initializedCommand: Command<[string], OptionValues> = initializeCommand(command); + return configureCommandOptions(initializedCommand); +} + +export function handleObNewActions(options: HandleObNewActionOptions): void { + const cmdOptions: ObNewOptions = convertOptionPropertyNames(options.command.opts() as ObNewOptions); + try { + runNgNewAngularWorkspace(options.projectName, cmdOptions.prefix as string); + runAddOblique(cmdOptions, options.projectName); + } catch (error) { + console.error('Installation failed: ', error); + } +} + +export function createAddObliqueCommand( + command: string, + options: Record, + projectName: string +): string { + const commandOptions: string[] = []; + const properties = obCliNewSchema.properties as Record; + for (const [key, value] of Object.entries(options)) { + const propertyOptions: ObNewSchemaOption = properties[key]; + if (Object.prototype.hasOwnProperty.call(propertyOptions, 'type')) { + const optionsList: string[] = + propertyOptions.type === 'boolean' + ? addBooleanFlag(key, value as boolean) + : addStringFlag({key, value: value as string}, projectName, propertyOptions); + commandOptions.push(...optionsList); + } + } + return [command, ...commandOptions].join(' '); +} + +function initializeCommand(command: Command<[string], OptionValues>): Command<[string], OptionValues> { + command + .name('new') + .version(cliPackage.version, optionDescriptions.ob.version.flags, optionDescriptions.ob.version.description) + .helpOption(optionDescriptions.new.help.flags, optionDescriptions.new.help.description) + .usage(commandUsageText('new')) + .summary(obNewConfig.obNewSummaryText) + .description(obNewConfig.obNewSummaryText) + .argument(obNewConfig.projectNameArgument.argumentName, obNewConfig.projectNameArgument.description) + .action(projectName => handleAction({projectName, command})) + .showSuggestionAfterError(true) + .showHelpAfterError(true); + return command; +} + +export function handleAction(options: HandleObNewActionOptions): void { + startObCommand(handleObNewActions as (options: HandleObNewActionOptions) => void, 'Oblique CLI ob new completed in', options); +} + +function configureCommandOptions(newCommand: Command<[string], OptionValues>): Command<[string], OptionValues> { + const commandWithOptions = addObNewCommandOptions(obCliNewSchema, newCommand); + return addImmutableOptionsText(commandWithOptions); +} + +export function addImmutableOptionsText(command: Command<[string], OptionValues>): Command<[string], OptionValues> { + command.addHelpText('after', '\nThese options are set per default:\n'); + const padEnd = 36; + + Object.entries(immutableOptions).forEach(([key, flag]) => { + const flagValue = + Object.prototype.hasOwnProperty.call(flag, 'value') && flag.value !== undefined && flag.value.length > 0 ? `=${flag.value}` : ''; + const newFlagValue = ` --${key}${flagValue}`.padEnd(padEnd, ' '); + command.addHelpText('after', `${newFlagValue} ${flag.description}`); + }); + return command; +} + +function getApplicationDirectory(projectName: string): string { + return [process.cwd(), projectName].join('/'); +} + +export function runAddOblique(options: ObNewOptions, projectName: string): void { + const command = createAddObliqueCommand(ngAddStringCommand, options, projectName); + const dir: string = getApplicationDirectory(projectName); + installMaterial(dir); + execSync(command, {stdio: 'inherit', cwd: dir}); + console.info(`[Complete]: Oblique added`); +} + +function installMaterial(dir: string): void { + console.info(`[Info]: Installs Angular Material`); + execSync(`npm install ${getVersionedDependency('@angular/material')}`, {stdio: 'inherit', cwd: dir}); +} + +export function runNgNewAngularWorkspace(projectName?: string, prefix?: string | 'app'): void { + console.info(createsWorkspaceMessage); + const options: string[] = []; + Object.entries(immutableOptions).forEach(([key, flag]) => { + options.push(`--${key} ${!key.startsWith('no') && Object.prototype.hasOwnProperty.call(flag, 'value') ? flag.value : ''}`); + }); + execSync(`npx ${getVersionedDependency('@angular/cli')} new ${projectName} ${options.join(' ')} --prefix=${prefix}`, { + stdio: 'inherit', + cwd: process.cwd() + }); +} + +function addBooleanFlag(key: string, value: boolean): string[] { + const commandOptions: string[] = []; + if (value) { + commandOptions.push(`--${key}`); + } else { + commandOptions.push(`--no-${key}`); + } + return commandOptions; +} + +export function getTitlesCommandOption(option: {key: string; value: string}, projectName: string): string { + if (option.key === 'title') { + if (option.value === projectNamePlaceholder || option.value === '') { + return `--${option.key}="${projectName}"`; + } + return `--${option.key}="${option.value}"`; + } + return ''; +} + +export function addStringFlag(option: {key: string; value: string}, projectName: string, property: ObNewSchemaOption): string[] { + const commandOptions: string[] = []; + if (Object.prototype.hasOwnProperty.call(property, 'type') && property.type === 'string') { + if (option.key === 'title') { + commandOptions.push(getTitlesCommandOption(option, projectName)); + } else { + commandOptions.push(`--${option.key}="${option.value}"`); + } + } + return commandOptions; +} diff --git a/projects/cli/src/new/schema.json b/projects/cli/src/new/schema.json new file mode 100644 index 000000000..a066610a7 --- /dev/null +++ b/projects/cli/src/new/schema.json @@ -0,0 +1,134 @@ +{ + "$id": "oblique-cli-ng-new-schema", + "title": "Oblique CLI ng new schema", + "properties": { + "title": { + "type": "string", + "defaultValue": "", + "description": "Add the specified application's title: The title will be visible in the header of your application.", + "defaultValueDescription": "project name.", + "minLength": 1, + "mandatory": true, + "flagValuePlaceholder": "" + }, + "locales": { + "type": "string", + "flagValuePlaceholder": "", + "defaultValue": "de-CH fr-CH it-CH", + "description": "Supported locales: Use a whitespace separated list." + }, + "environments": { + "type": "string", + "defaultValue": "local dev ref test abn prod", + "flagValuePlaceholder": "", + "defaultValueDescription": "local dev ref test abn prod", + "description": "Environment files: Use a whitespace separated list or leave a whitespace to skip the feature. 'local' will create an 'environment.ts' file, all other environments will create a corresponding 'environment..ts' file." + }, + "prefix": { + "type": "string", + "flagValuePlaceholder": "", + "defaultValue": "app", + "description": "Prefix configuration: The prefix for components and directive's selectors. Leave empty for no prefix." + }, + "proxy": { + "type": "string", + "flagValuePlaceholder": "", + "defaultValue": " ", + "description": "Proxy configuration: Defines the port for the proxy configuration for server connection." + }, + "jenkins": { + "type": "string", + "flagValuePlaceholder": "", + "defaultValue": "", + "defaultValueDescription": "no jenkins configuration", + "description": "Jenkins configuration: Defines Jenkins / CF configuration. For example (ORG;APP). Leave empty for no Jenkins / CF configuration." + }, + "ajv": { + "type": "boolean", + "defaultValue": true, + "description": "Add Ajv dependency: Value \"true\" enables form validation based on a schema delivered by the server.", + "resources": ["https://www.npmjs.com/package/ajv"] + }, + "unknownRoute": { + "type": "boolean", + "defaultValue": true, + "description": "Unknown route management: This will display custom 404 pages instead of redirecting to the home page.", + "resources": [ + "Oblique's Unknown route API at https://oblique.bit.admin.ch/helpers/unknown-route/api" + ] + }, + "httpInterceptors": { + "type": "boolean", + "defaultValue": true, + "description": "Http interceptor: If set to true, it will provide the ObHttpApiInterceptor in the app.module.ts. The interceptor displays a spinner on API calls and a notification on errors.", + "resources": [ + "Oblique's ObHttpInterceptor API at https://oblique.bit.admin.ch/helpers/http-interceptor/api" + ] + }, + "banner": { + "type": "boolean", + "defaultValue": false, + "description": "Banner to show current environment: The ObBanner will show the current environment in the header. This feature is only available if at least 1 environment will be defined. To define your environments, use the option --environments .", + "resources": [ + "Oblique's ObBanner API at https://oblique.bit.admin.ch/helpers/banner/api" + ] + }, + "externalLink": { + "type": "boolean", + "defaultValue": true, + "description": "External link: If true, it imports the ObExternalLinkModule. This feature automatically enhances external links.", + "resources": [ + "Oblique's External link API at https://oblique.bit.admin.ch/components/external-link/api" + ] + }, + "jest": { + "type": "boolean", + "defaultValue": true, + "description": "Jest for unit tests: If true, Karma/Jasmine will be replaced with Jest as your application's testing framework.", + "resources": [ + "Jest at npm https://www.npmjs.com/package/jest", + "Jest's documentation: https://jestjs.io/docs/getting-started" + ] + }, + "protractor": { + "type": "boolean", + "defaultValue": false, + "description": "Protractor for end to end tests: If you use this flag, you keep Protractor for e2e tests." + }, + "npmrc": { + "type": "boolean", + "defaultValue": true, + "description": "Create .npmrc: If you use this flag, it adds an .npmrc file, suitable for projects located within confederation/federal network." + }, + "sonar": { + "type": "boolean", + "defaultValue": true, + "description": "Sonar configuration: If set to true, a Sonar configuration is added." + }, + "static": { + "type": "boolean", + "defaultValue": false, + "description": "Cloud foundry static build pack: It adds the static build back for CF.", + "resources": [ + "Cloud Foundry Documentation at https://docs.cloudfoundry.org/buildpacks/staticfile" + ] + }, + "eslint": { + "type": "boolean", + "defaultValue": true, + "description": "ESLint & Prettier: If true, it adds eslint & prettier configuration as used by the Oblique team.", + "resources": [ + "ESLint Documentation: https://eslint.org/docs/latest/use/getting-started" + ] + }, + "husky": { + "type": "boolean", + "defaultValue": true, + "description": "Husky configuration: If true, it adds git hooks to automatically format changed files.", + "resources": [ + "Husky Documentation at https://typicode.github.io/husky/", + "Husky's package at npm https://www.npmjs.com/package/husky" + ] + } + } +} diff --git a/projects/cli/src/utils/cli-utils.spec.ts b/projects/cli/src/utils/cli-utils.spec.ts index 49900baa4..fb2f1d18d 100644 --- a/projects/cli/src/utils/cli-utils.spec.ts +++ b/projects/cli/src/utils/cli-utils.spec.ts @@ -1,220 +1,241 @@ +import {handleAction} from '../index'; import { - optionDescriptions as cliOptions, commandUsageText, createAdditionalHelpText, exampleUsageText, getHelpText, - obTitle, + obExamples, + optionDescriptions, runObCommand, startObCommand, titleText } from './cli-utils'; -import SpyInstance = jest.SpyInstance; -test('cliOptions.version.flags should be correct', () => { - expect(cliOptions.ob.version.flags).toBe('-v, --version'); -}); - -test('cliOptions.version.description should be correct', () => { - expect(cliOptions.ob.version.description).toBe('Shows the current version of @oblique/cli'); -}); +// Mock console methods to capture their outputs +console.info = jest.fn(); +console.time = jest.fn(); +console.timeEnd = jest.fn(); -test('cliOptions.version.command should be correct', () => { - expect(cliOptions.ob.version.command).toBe('ob -v'); -}); - -test('obTitle should have correct value', () => { - expect(obTitle).toBe('Oblique Cli'); -}); +describe('CLI Utils', () => { + describe('optionDescriptions', () => { + test('optionDescriptions.ob.version.description should be correct', () => { + expect(optionDescriptions.ob.version.description).toBe('Shows the current version of @oblique/cli'); + }); -test('cliOptions.help.flags should be correct', () => { - expect(cliOptions.ob.help.flags).toBe('-h, --help'); -}); - -test('cliOptions.help.description should be correct', () => { - expect(cliOptions.ob.help.description).toBe('Shows a help message for the "ob" command in the console.'); -}); - -test('cliOptions.help.command should be correct', () => { - expect(cliOptions.ob.help.command).toBe('ob -h'); -}); + test('optionDescriptions.ob.help.description should be correct', () => { + expect(optionDescriptions.ob.help.description).toBe(getHelpText('ob')); + }); -test('runObCommand should log correct message', () => { - const consoleSpy = jest.spyOn(console, 'info').mockImplementation(() => {}); - runObCommand(); - expect(consoleSpy).toHaveBeenCalledWith( - '\n Use `ob new ` to create a new project\n Use `ob --help` to explore the available commands\n' - ); - consoleSpy.mockRestore(); -}); - -test('titleText should return formatted title with default delimiters', () => { - const title = 'Oblique Cli'; - expect(titleText(title)).toBe(`\n${title}\n`); -}); - -test('titleText should return formatted title with custom delimiters', () => { - const title = 'Oblique Cli'; - expect(titleText(title, '>>', '<<')).toBe(`>>${title}<<`); -}); - -test('getHelpText should return correct help text', () => { - expect(getHelpText('ob')).toBe('Shows a help message for the "ob" command in the console.'); -}); - -describe('startObCommand', () => { - let callback = jest.fn(); - let consoleInfoSpy: SpyInstance; - let consoleTimeSpy: SpyInstance; - let consoleTimeEndSpy: SpyInstance; - - beforeEach(() => { - consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation(() => {}); - consoleTimeSpy = jest.spyOn(console, 'time').mockImplementation(() => {}); - consoleTimeEndSpy = jest.spyOn(console, 'timeEnd').mockImplementation(() => {}); - callback = jest.fn(); + test('optionDescriptions.new.help.description should be correct', () => { + expect(optionDescriptions.new.help.description).toBe(getHelpText('ob new')); + }); }); - afterEach(() => { - consoleInfoSpy.mockRestore(); - consoleTimeSpy.mockRestore(); - consoleTimeEndSpy.mockRestore(); + describe('obExamples', () => { + test('obExamples should be correct', () => { + expect(obExamples).toEqual([ + {command: optionDescriptions.ob.version.command, description: optionDescriptions.ob.version.description}, + {command: optionDescriptions.ob.help.command, description: optionDescriptions.ob.help.description}, + {command: optionDescriptions.new.obNewCommand.command, description: optionDescriptions.new.obNewCommand.description}, + {command: optionDescriptions.new.help.command, description: optionDescriptions.new.help.description} + ]); + }); }); - test('should call callback with options', () => { - const options = {key: 'value'}; - const label = 'Test Label'; - - startObCommand(callback, label, options); - - expect(callback).toHaveBeenCalledWith(options); + describe('runObCommand', () => { + test('runObCommand should log the correct messages', () => { + runObCommand(); + expect(console.info).toHaveBeenCalledWith(expect.stringContaining(`Use \`ob new \` to create a new project`)); + }); + + test('runObCommand should log the help command message', () => { + runObCommand(); + expect(console.info).toHaveBeenCalledWith(expect.stringContaining('Use `ob --help` to explore the available commands')); + }); + + test('runObCommand should log the new help command message', () => { + runObCommand(); + expect(console.info).toHaveBeenCalledWith( + expect.stringContaining('Or use `ob new --help` to explore the available options for the ob new command') + ); + }); }); - test('should log start time with label', () => { - const options = {key: 'value'}; - const label = 'Test Label'; + describe('startObCommand', () => { + test('should called startObCommand in handleAction', () => { + // eslint-disable-next-line @typescript-eslint/no-var-requires,@typescript-eslint/no-unsafe-assignment + const cliUtils = require('../utils/cli-utils.ts'); + jest.spyOn(cliUtils, 'startObCommand'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access + handleAction({}); + expect(startObCommand).toHaveBeenLastCalledWith(runObCommand, 'Oblique CLI completed in', {}); + }); - startObCommand(callback, label, options); + test('startObCommand should start the timer', () => { + const mockCallback = jest.fn(); + const label = 'test label'; + const options = {test: 'test'}; - expect(consoleTimeSpy).toHaveBeenCalledWith(label); - }); + startObCommand(mockCallback, label, options); - test('should log end time with label', () => { - const options = {key: 'value'}; - const label = 'Test Label'; + expect(console.time).toHaveBeenCalledWith(label); + }); - startObCommand(callback, label, options); + test('startObCommand should execute the callback', () => { + const mockCallback = jest.fn(); + const label = 'test label'; + const options = {test: 'test'}; - expect(consoleTimeEndSpy).toHaveBeenCalledWith(label); - }); + startObCommand(mockCallback, label, options); + + expect(mockCallback).toHaveBeenCalledWith(options); + }); - test('should log title', () => { - const title = 'Oblique Cli'; - const uppercaseTitle = title.toUpperCase(); - const options = {key: 'value'}; - const label = 'Test Label'; + test('startObCommand should end the timer', () => { + const mockCallback = jest.fn(); + const label = 'test label'; + const options = {test: 'test'}; - startObCommand(callback, label, options); + startObCommand(mockCallback, label, options); - expect(consoleInfoSpy).toHaveBeenCalledWith(uppercaseTitle); + expect(console.timeEnd).toHaveBeenCalledWith(label); + }); }); -}); -test('commandUsageText with default parameters should return " [option]"', () => { - expect(commandUsageText()).toBe(' [option]'); -}); + describe('getHelpText', () => { + test('getHelpText("ob") should return correct help text', () => { + expect(getHelpText('ob')).toBe('Shows a help message for the "ob" command in the console'); + }); -test('commandUsageText with "new" parameter should return "new [option]"', () => { - expect(commandUsageText('new')).toBe('new [option]'); -}); + test('getHelpText("ob new") should return correct help text', () => { + expect(getHelpText('ob new')).toBe('Shows a help message for the "ob new" command in the console'); + }); + }); -test('commandUsageText with "update" and "--force" parameters should return "update --force"', () => { - expect(commandUsageText('update', '--force')).toBe('update --force'); -}); + describe('commandUsageText', () => { + test.each([ + {property: `new` as 'new' | 'update' | '', expected: ` [...options]`}, + {property: `update` as 'new' | 'update' | '', expected: `update [...options]`}, + {property: `` as 'new' | 'update' | '', expected: ` [...options]`}, + {property: undefined as 'new' | 'update' | '', expected: ` [...options]`} + ])('commandUsageText("$property") should return correct usage text', ({property, expected}) => { + expect(commandUsageText(property)).toBe(expected); + }); + + test('commandUsageText("") should return correct usage text', () => { + expect(commandUsageText('')).toBe(' [...options]'); + }); + + test('commandUsageText("update") should return correct usage text', () => { + expect(commandUsageText('update')).toBe('update [...options]'); + }); + }); -test('exampleUsageText should return formatted examples', () => { - const examples = [ - {command: 'ob new', description: 'Creates a new project'}, - {command: 'ob update', description: 'Updates the project'} - ]; - const expectedOutput = ` + describe('exampleUsageText', () => { + test('exampleUsageText should return formatted examples', () => { + const examples = [ + {command: 'ob new', description: 'Creates a new project'}, + {command: 'ob update', description: 'Updates the project'} + ]; + const expectedOutput = ` Examples of use: - ob newCreates a new project - ob updateUpdates the project`; - const result = exampleUsageText(examples); - expect(result).toBe(expectedOutput); -}); - -describe('createAdditionalHelpText', () => { - const spaceUnit = ' '; - - test('should return a properly formatted string', () => { - const title = 'Usage'; - const examples = [ - {command: 'cmd1', description: 'description1'}, - {command: 'cmd2', description: 'description2'} - ]; - const maxCommandWidth = 4; - - const result = createAdditionalHelpText(title, examples, maxCommandWidth); - const expected = ['Usage', ` ${spaceUnit}cmd1 description1\n${spaceUnit} cmd2 description2`].join(''); - - expect(result).toBe(expected); +\tob newCreates a new project +\tob updateUpdates the project`; + const result = exampleUsageText(examples); + expect(result).toBe(expectedOutput); + }); }); - test('should handle empty examples array', () => { - const title = 'Usage'; - const examples: {command: string; description: string}[] = []; - const maxCommandWidth = 4; - - const result = createAdditionalHelpText(title, examples, maxCommandWidth); - const expected = 'Usage'; - - expect(result).toBe(expected); + describe('createAdditionalHelpText', () => { + test('should return a properly formatted string', () => { + const title = 'Usage'; + const examples = [ + {command: 'cmd1', description: 'description1'}, + {command: 'cmd2', description: 'description2'} + ]; + const maxCommandWidth = 4; + + const result = createAdditionalHelpText(title, examples, maxCommandWidth); + const expected = ['Usage', `\tcmd1 description1\n\tcmd2 description2`].join(''); + + expect(result).toBe(expected); + }); + + test('should handle empty examples array', () => { + const title = 'Usage'; + const examples: {command: string; description: string}[] = []; + const maxCommandWidth = 4; + + const result = createAdditionalHelpText(title, examples, maxCommandWidth); + const expected = 'Usage'; + + expect(result).toBe(expected); + }); + + test('should pad commands correctly with given maxCommandWidth', () => { + const title = 'Usage'; + const examples = [ + {command: 'cmd1', description: 'description1'}, + {command: 'cmd2', description: 'description2'} + ]; + const maxCommandWidth = 6; + + const result = createAdditionalHelpText(title, examples, maxCommandWidth); + const expected = ['Usage', `\tcmd1 description1\n\tcmd2 description2`].join(''); + + expect(result).toBe(expected); + }); + + test('should handle longer commands correctly without truncation', () => { + const title = 'Usage'; + const examples = [ + {command: 'longcommand1', description: 'description1'}, + {command: 'cmd2', description: 'description2'} + ]; + const maxCommandWidth = examples[0].command.length; + + const result = createAdditionalHelpText(title, examples, maxCommandWidth); + const expected = ['Usage', `\tlongcommand1 description1\n\tcmd2 description2`].join(''); + + expect(result).toBe(expected); + }); + }); + describe('exampleUsageText', () => { + test('exampleUsageText should return formatted example usage text', () => { + const examples = [ + {command: 'ob -v', description: ' Shows the current version of @oblique/cli'}, + {command: 'ob -h', description: ' Shows a help message for the "ob" command in the console'} + ]; + const expectedOutput = + '\nExamples of use:\n\tob -v Shows the current version of @oblique/cli\n\tob -h Shows a help message for the "ob" command in the console'; + const result = exampleUsageText(examples); + expect(result).toBe(expectedOutput); + }); }); - test('should pad commands correctly with given maxCommandWidth', () => { - const title = 'Usage'; - const examples = [ - {command: 'cmd1', description: 'description1'}, - {command: 'cmd2', description: 'description2'} - ]; - const maxCommandWidth = 5; - - const result = createAdditionalHelpText(title, examples, maxCommandWidth); - const expected = ['Usage', `${spaceUnit}cmd1 description1\n${spaceUnit}cmd2 description2`].join(''); - - expect(cleanOutput(result)).toBe(cleanOutput(expected)); + describe('createAdditionalHelpText', () => { + test('createAdditionalHelpText should return help text with padded commands', () => { + const title = '\nExample usages:\n'; + const examples = [ + {command: 'ob -v', description: 'Shows the current version of @oblique/cli'}, + {command: 'ob -h', description: 'Shows a help message for the "ob" command in the console'} + ]; + const maxCommandWidth = 5; + const expectedOutput = + '\nExample usages:\n\tob -v Shows the current version of @oblique/cli\n\tob -h Shows a help message for the "ob" command in the console'; + expect(createAdditionalHelpText(title, examples, maxCommandWidth)).toBe(expectedOutput); + }); }); - test('should handle longer commands correctly without truncation', () => { - const title = 'Usage'; - const examples = [ - {command: 'longcommand1 ', description: 'description1'}, - {command: 'cmd2', description: 'description2'} - ]; - - const result = createAdditionalHelpText(title, examples, examples[0].command.length); - - expect(result).toBe( - [ - [ - title, - ' ', - examples[0].command, - ' ', - examples[0].description, - '\n ', - examples[1].command, - ' ', - examples[1].description - ].join('') - ].join('') - ); + describe('titleText', () => { + test('titleText should return title text with default delimiters', () => { + const title = 'Test Title'; + expect(titleText(title)).toBe('\nTest Title\n'); + }); + + test('titleText should return title text with custom delimiters', () => { + const title = 'Test Title'; + expect(titleText(title, '', ' - ')).toBe('Test Title - '); + }); }); }); - -function cleanOutput(output: Buffer | string): string { - const outputString = output.toString(); - return outputString.replace(/\s+/g, ' ').trim(); -} diff --git a/projects/cli/src/utils/cli-utils.ts b/projects/cli/src/utils/cli-utils.ts index df6698978..c8dcded72 100644 --- a/projects/cli/src/utils/cli-utils.ts +++ b/projects/cli/src/utils/cli-utils.ts @@ -1,4 +1,10 @@ -#!/usr/bin/env node +export const currentVersions = { + /* eslint-disable @typescript-eslint/naming-convention */ + '@oblique/oblique': '12', + '@angular/cli': '18', + '@angular/material': '18' + /* eslint-enable @typescript-eslint/naming-convention */ +} as const; export const optionDescriptions = { ob: { @@ -12,28 +18,42 @@ export const optionDescriptions = { description: getHelpText('ob'), command: 'ob -h' } + }, + new: { + obNewCommand: { + command: 'ob new [...options]', + description: 'Create a new Oblique project' + }, + help: { + flags: '-h, --help', + description: getHelpText('ob new'), + command: 'ob new -h' + } } }; export const obExamples = [ {command: optionDescriptions.ob.version.command, description: optionDescriptions.ob.version.description}, - {command: optionDescriptions.ob.help.command, description: optionDescriptions.ob.help.description} + {command: optionDescriptions.ob.help.command, description: optionDescriptions.ob.help.description}, + {command: optionDescriptions.new.obNewCommand.command, description: optionDescriptions.new.obNewCommand.description}, + {command: optionDescriptions.new.help.command, description: optionDescriptions.new.help.description} ]; -const spaceUnit = ` `; -const projectNamePlaceholder = ``; +const spaceUnit = `\t`; +export const projectNamePlaceholder = ``; export const runObCommand = (): void => { console.info( `\n${spaceUnit}Use \`ob new ${projectNamePlaceholder}\` to create a new project\n` + - `${spaceUnit}Use \`ob --help\` to explore the available commands\n` + `${spaceUnit}Use \`ob --help\` to explore the available commands\n` + + `${spaceUnit}Or use \`ob new --help\` to explore the available options for the ob new command\n` ); }; export const obTitle = `Oblique Cli`; -export function getHelpText(command: 'ob'): string { - return `Shows a help message for the "${command}" command in the console.`; +export function getHelpText(command: 'ob' | 'ob new'): string { + return `Shows a help message for the "${command}" command in the console`; } export const startObCommand = (callback: (options: T) => void, label: string, options: T): void => { @@ -43,7 +63,10 @@ export const startObCommand = (callback: (options: T) => void, label: string, console.timeEnd(label); }; -export function commandUsageText(subCommand: '' | 'new' | 'update' = '', option = '[option]'): string { +export function commandUsageText(subCommand: '' | 'new' | 'update' = '', option = '[...options]'): string { + if (subCommand === 'new') { + return `${projectNamePlaceholder} ${option}`; + } return `${subCommand} ${option}`; } @@ -66,3 +89,7 @@ export function createAdditionalHelpText( export function titleText(title: string, delimiterStart = '\n', delimiterEnd = '\n'): string { return `${delimiterStart}${title}${delimiterEnd}`; } + +export function getVersionedDependency(dependency: keyof typeof currentVersions): string { + return `${dependency}@${currentVersions[dependency]}`; +} diff --git a/projects/cli/src/utils/ob-cli.model.ts b/projects/cli/src/utils/ob-cli.model.ts new file mode 100644 index 000000000..c7870f1e3 --- /dev/null +++ b/projects/cli/src/utils/ob-cli.model.ts @@ -0,0 +1,3 @@ +export interface ObCliSchema { + properties: Type; +} diff --git a/projects/cli/src/utils/ob-configure-command.spec.ts b/projects/cli/src/utils/ob-configure-command.spec.ts new file mode 100644 index 000000000..f0d0f73e8 --- /dev/null +++ b/projects/cli/src/utils/ob-configure-command.spec.ts @@ -0,0 +1,165 @@ +import {Command, Option} from '@commander-js/extra-typings'; +import {OptionValues} from 'commander'; +import {ObNewOptions, ObNewSchemaOption} from '../new/ob-new.model'; +import {ObCliSchema} from './ob-cli.model'; +import {addObNewCommandOptions, configureOption, convertOptionPropertyNames} from './ob-configure-command'; + +jest.mock('../new/ob-new.model'); +jest.mock('./ob-cli.model'); +/* eslint-disable @typescript-eslint/strict-boolean-expressions*/ + +describe('ob-configure-command tests', () => { + describe('convertOptionPropertyNames', () => { + test('should convert first letter of option names to lowercase', () => { + const inputOptions = { + /* eslint-disable @typescript-eslint/naming-convention */ + SomeOption: 'value1', + aThirdOption: true, + anotherOption: 'value2' + } as unknown as ObNewOptions; + + const expectedOptions = { + someOption: 'value1', + anotherOption: 'value2', + aThirdOption: true + }; + + const result = convertOptionPropertyNames(inputOptions); + + expect(result).toEqual(expectedOptions); + }); + }); + + describe('addObNewCommandOptions', () => { + test('should add command options based on the schema', () => { + const schema: ObCliSchema>> = { + properties: { + option1: { + description: 'Description for option 1', + flagValuePlaceholder: 'value1' + }, + option2: { + description: 'Description for option 2', + shortFlag: 'o', + choices: ['choice1', 'choice2'] + } + } + } as unknown as ObCliSchema>>; + + const command = new Command<[string], OptionValues>('test-command'); + + const commandWithOptions = addObNewCommandOptions(schema, command); + expect(commandWithOptions.options.at(1).flags).toBe(`-o , --option2 `); + }); + + test('should throw an error if schema properties are not found', () => { + const schema = {} as ObCliSchema>>; + + const command = new Command<[string], OptionValues>('test-command'); + + expect(() => addObNewCommandOptions(schema, command)).toThrow('Schema for command ob test-command not found!'); + }); + }); + + describe('configureOption', () => { + let option: Option; + const config = { + description: 'Test option description', + shortFlag: 't', + defaultValue: 'defaultValue', + choices: ['choice1', 'choice2'], + mandatory: true, + flagValuePlaceholder: 'testValue' + } as ObNewSchemaOption; + option = configureOption(config, 'test-option'); + + /*eslint-disable @typescript-eslint/no-unsafe-assignment */ + const testCases = [ + {description: 'option flags to contain -t', actual: option.flags, expected: '-t', matcher: 'toContain'}, + {description: 'option flags to contain --test-option', actual: option.flags, expected: '--test-option', matcher: 'toContain'}, + {description: 'option flags to contain testValue', actual: option.flags, expected: 'testValue', matcher: 'toContain'}, + {description: 'default value to be defaultValue', actual: option.defaultValue, expected: 'defaultValue', matcher: 'toBe'}, + {description: 'mandatory to be true', actual: option.mandatory, expected: true, matcher: 'toBe'}, + { + description: 'argument choices to equal choice1 and choice2', + actual: option.argChoices, + expected: ['choice1', 'choice2'], + matcher: 'toEqual' + } + ]; + + test.each(testCases)('should have $description', ({actual, expected, matcher}) => { + if (matcher === 'toContain') { + expect(actual).toContain(expected); + } else if (matcher === 'toBe') { + expect(actual).toBe(expected); + } else if (matcher === 'toEqual') { + expect(actual).toEqual(expected); + } + }); + + test('should handle no short flag', () => { + option = configureOption( + { + description: 'Test option description', + flagValuePlaceholder: 'testValue', + type: 'string' + } as ObNewSchemaOption, + 'test-option' + ); + + expect(option.flags).not.toContain('--testValue'); + expect(option.flags).toContain('--test-option'); + }); + + test.each([ + { + description: 'longFlag is present and has length > 0', + optionConfig: {shortFlag: 's', description: 'Test option'} as ObNewSchemaOption, + longFlag: 'testOption', + expectedShortFlag: '-s', + expectedLongFlag: '--testOption' + }, + { + description: 'longFlag is an empty string', + optionConfig: {shortFlag: 's', description: 'Test option'} as ObNewSchemaOption, + longFlag: '', + expectedShortFlag: '-s', + expectedLongFlag: undefined + }, + { + description: 'longFlag is undefined', + optionConfig: {shortFlag: 's', description: 'Test option'} as ObNewSchemaOption, + longFlag: undefined, + expectedShortFlag: '-s', + expectedLongFlag: undefined + }, + { + description: 'longFlag is null', + optionConfig: {shortFlag: 's', description: 'Test option'} as ObNewSchemaOption, + longFlag: null, + expectedShortFlag: '-s', + expectedLongFlag: undefined + } + ])('should handle $description', ({optionConfig, longFlag, expectedShortFlag, expectedLongFlag}) => { + const result = configureOption(optionConfig, longFlag); + + if (expectedShortFlag) { + expect(result.flags).toContain(expectedShortFlag); + } + if (expectedLongFlag) { + expect(result.flags).toContain(expectedLongFlag); + } else { + expect(result.flags).not.toContain('--'); + } + }); + + test('should handle no choices', () => { + const optionConfig = { + description: 'Test option description' + } as ObNewSchemaOption; + const optionWithoutChoice = configureOption(optionConfig, 'test-option'); + expect(optionWithoutChoice.argChoices).toBeUndefined(); + }); + }); +}); diff --git a/projects/cli/src/utils/ob-configure-command.ts b/projects/cli/src/utils/ob-configure-command.ts new file mode 100644 index 000000000..58bf8e25a --- /dev/null +++ b/projects/cli/src/utils/ob-configure-command.ts @@ -0,0 +1,72 @@ +import {Command, Option, OptionValues} from '@commander-js/extra-typings'; +import {ObNewOptions, ObNewSchemaOption, OptionKeys} from '../new/ob-new.model'; +import {ObCliSchema} from './ob-cli.model'; + +//this is needed because commander sometimes converts the option to firstUpperCase +export function convertOptionPropertyNames(options: ObNewOptions): ObNewOptions { + const optionRecord: ObNewOptions = {} as ObNewOptions; + for (const [key, value] of Object.entries(options)) { + // this is needed because Commander adds the property name sometimes with first letter uppercase + optionRecord[convertFirstLetterLowerCase(key) as OptionKeys] = value; + } + return optionRecord; +} + +export function addObNewCommandOptions( + schema: ObCliSchema>>, + command: Command<[string], OptionValues> +): Command<[string], OptionValues> { + if (Object.prototype.hasOwnProperty.call(schema, 'properties')) { + for (const [key, value] of Object.entries(schema.properties)) { + value.description = createOptionDescription(value.description, value.resources); + command.addOption(configureOption(value, key)); + } + } else { + throw new Error(`Schema for command ob ${command.name()} not found!`); + } + return command; +} + +export function configureOption(config: ObNewSchemaOption, longFlag: string): Option { + const shortFlag = (config.shortFlag ?? '').length > 0 ? `-${config.shortFlag}` : ''; + const longFlagOption = longFlag && longFlag.length > 0 ? createFlagText(shortFlag, longFlag) : ''; + const flagValuePlaceholder = config.flagValuePlaceholder ?? ''; + const option = new Option(`${shortFlag} ${longFlagOption} ${flagValuePlaceholder}`, config.description); + const optionWithMandatory = addMandatory(option, config.mandatory); + const optionWithDefault = addDefaultValue(optionWithMandatory, config.defaultValue, config.defaultValueDescription); + return addChoices(optionWithDefault, config.choices); +} + +function createFlagText(shortFlag: string, optionName: string): string { + return `${shortFlag.length > 0 ? ',' : ''} --${optionName}`; +} + +function createOptionDescription(description: string, resources?: string[]): string { + let resourceDescription = resources && resources.length > 0 ? resources.join(' and ') : ''; + resourceDescription = resources && resources.length > 0 ? ` See more information at ${resourceDescription}` : ''; + return [description, resourceDescription].join(''); +} + +function addMandatory(option: Option, mandatory?: boolean): Option { + return mandatory === true ? option.makeOptionMandatory(true) : option; +} + +function addDefaultValue(option: Option, defaultValue?: boolean | string, defaultValueDescription?: string): Option { + if (defaultValue !== undefined) { + option.defaultValue = defaultValue; + option.default(defaultValue, defaultValueDescription); + } + if (defaultValueDescription !== undefined) { + option.defaultValueDescription = defaultValueDescription; + option.default(defaultValue, defaultValueDescription); + } + return option; +} + +function addChoices(option: Option, choices?: string[]): Option { + return choices && choices.length > 0 ? option.choices(choices) : option; +} + +function convertFirstLetterLowerCase(input: string): string { + return input.charAt(0).toLowerCase() + input.slice(1); +} From e2200e3fa14e8ed997a50c51af6a616812331f6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Giller?= Date: Thu, 28 Nov 2024 14:18:50 +0100 Subject: [PATCH 20/50] feat(cli/toolchain): add `link` command This command builds the CLI and calls `npm link` on it allowing it to be used globally OUI-3243 --- projects/cli/README.md | 1 + projects/cli/package.json | 1 + 2 files changed, 2 insertions(+) diff --git a/projects/cli/README.md b/projects/cli/README.md index f80d718d7..c69e07c95 100644 --- a/projects/cli/README.md +++ b/projects/cli/README.md @@ -14,6 +14,7 @@ You will find information about how to use Oblique, its CLI, code samples, FAQ a ## Scripts +- **link**: builds the CLI and call `npm link` on the artifact to allow the CLI to be called globally - **lint**: lints the project with EsLint and Prettier; Automatically run on the CI pipeline - **format**: same as lint, but with autofix parameter - **test**: run all tests and collects coverage diff --git a/projects/cli/package.json b/projects/cli/package.json index a8cb2f188..db1a68144 100644 --- a/projects/cli/package.json +++ b/projects/cli/package.json @@ -13,6 +13,7 @@ }, "scripts": { "start": "ts-node src/index.ts", + "link": "npm uninstall @oblique/cli -g && npm run build && cd ../../dist/cli && npm link", "lint": "ts-node scripts/lint.ts", "format": "npm run lint -- --fix", "test": "jest", From 7e504db2ca51cddc19647ca5e41bb826fe68ce75 Mon Sep 17 00:00:00 2001 From: Nina Date: Tue, 26 Nov 2024 08:11:57 +0100 Subject: [PATCH 21/50] fix(oblique/material): remove margin-top from all chip variants OUI-3400 --- projects/oblique/src/styles/scss/material/_mat-chip.scss | 6 ------ 1 file changed, 6 deletions(-) diff --git a/projects/oblique/src/styles/scss/material/_mat-chip.scss b/projects/oblique/src/styles/scss/material/_mat-chip.scss index beed62333..77fcf4987 100644 --- a/projects/oblique/src/styles/scss/material/_mat-chip.scss +++ b/projects/oblique/src/styles/scss/material/_mat-chip.scss @@ -107,12 +107,6 @@ } } -.mat-mdc-chip-grid, -.mat-mdc-chip-set, -.mat-mdc-chip-listbox { - margin-top: variables.$ob-spacing-lg; -} - .mat-mdc-form-field { &.mat-mdc-form-field-type-mat-chip-grid { .mat-mdc-text-field-wrapper.mdc-text-field, From a83f64b15c9fa6e3bcddd6c20c32ee7adc9d46f2 Mon Sep 17 00:00:00 2001 From: Nina Date: Tue, 26 Nov 2024 09:08:10 +0100 Subject: [PATCH 22/50] fix(oblique/material): ensure chips autocomplete is the same height as a form field OUI-3400 --- projects/oblique/src/styles/scss/material/_mat-chip.scss | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/projects/oblique/src/styles/scss/material/_mat-chip.scss b/projects/oblique/src/styles/scss/material/_mat-chip.scss index 77fcf4987..3b82ea43d 100644 --- a/projects/oblique/src/styles/scss/material/_mat-chip.scss +++ b/projects/oblique/src/styles/scss/material/_mat-chip.scss @@ -112,10 +112,13 @@ .mat-mdc-text-field-wrapper.mdc-text-field, .mat-mdc-text-field-wrapper.mdc-text-field .mdc-notched-outline { max-height: none; + } - .mat-mdc-chip { - margin-top: 8px; - margin-bottom: 0; + mat-chip-row, + mat-chip-option, + mat-chip { + &.mdc-evolution-chip { + margin: 1px 4px 4px 4px; } } } From c78d591f192909c568a361f5b471c4090580ae1b Mon Sep 17 00:00:00 2001 From: Nina Date: Tue, 26 Nov 2024 09:09:05 +0100 Subject: [PATCH 23/50] fix(sds/material): add missing label to chips autocomplete example OUI-3400 --- .../chips-example-autocomplete-preview.component.html | 1 + 1 file changed, 1 insertion(+) diff --git a/projects/sds/src/app/code-examples/code-examples/chips/previews/autocomplete/chips-example-autocomplete-preview.component.html b/projects/sds/src/app/code-examples/code-examples/chips/previews/autocomplete/chips-example-autocomplete-preview.component.html index 2069b5645..24256cc46 100644 --- a/projects/sds/src/app/code-examples/code-examples/chips/previews/autocomplete/chips-example-autocomplete-preview.component.html +++ b/projects/sds/src/app/code-examples/code-examples/chips/previews/autocomplete/chips-example-autocomplete-preview.component.html @@ -1,4 +1,5 @@ + Chips autocomplete example {{ fruit }} From 3bfadcdc3b832669ca73428351880312040f6e66 Mon Sep 17 00:00:00 2001 From: Nina Date: Tue, 26 Nov 2024 09:12:36 +0100 Subject: [PATCH 24/50] fix(sandbox/material): add missing label to chips autocomplete example OUI-3400 --- projects/sandbox/src/app/material/chips/chips.component.html | 1 + 1 file changed, 1 insertion(+) diff --git a/projects/sandbox/src/app/material/chips/chips.component.html b/projects/sandbox/src/app/material/chips/chips.component.html index 731b60972..0ad2bca43 100644 --- a/projects/sandbox/src/app/material/chips/chips.component.html +++ b/projects/sandbox/src/app/material/chips/chips.component.html @@ -21,6 +21,7 @@ + Chips autocomplete From 11bcc11d4f39569009cdedf41b29901353e3f1eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Giller?= Date: Thu, 28 Nov 2024 16:32:22 +0100 Subject: [PATCH 25/50] fix(oblique/material): don't mix CSS pseudo-class with `not` This causes Angular to trigger a warning on build OUI-3344 --- projects/oblique/src/styles/scss/material/_mat-list.scss | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/projects/oblique/src/styles/scss/material/_mat-list.scss b/projects/oblique/src/styles/scss/material/_mat-list.scss index 7dfb86d4d..b179c8581 100644 --- a/projects/oblique/src/styles/scss/material/_mat-list.scss +++ b/projects/oblique/src/styles/scss/material/_mat-list.scss @@ -71,10 +71,13 @@ border-color: variables.$ob-secondary-600; background-color: variables.$ob-secondary-600; } - - &:not(:checked, :indeterminate, [data-indeterminate="true"]) ~ .mdc-checkbox__background { + /* stylelint-disable selector-not-notation */ + // because Angular don't support pseudo-class together with the complex selector-not-notation + &:not(:checked):not(:indeterminate):not([data-indeterminate="true"]) + ~ .mdc-checkbox__background { border-color: variables.$ob-secondary-600; } + /* stylelint-enable selector-not-notation */ } .ob-list-item-image { From 5443bce967aad481c76c2c1ef9eb0605225717e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Giller?= Date: Fri, 29 Nov 2024 22:31:34 +0100 Subject: [PATCH 26/50] feat(sds/banner): avoid conflict with global `banner` class The `.banner` class is already globally defined for the Banner feature's API OUI-3331 --- projects/sds/src/app/app.component.html | 2 +- projects/sds/src/app/app.component.scss | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/projects/sds/src/app/app.component.html b/projects/sds/src/app/app.component.html index cea0c4cc2..8c77774e3 100644 --- a/projects/sds/src/app/app.component.html +++ b/projects/sds/src/app/app.component.html @@ -1,4 +1,4 @@ -