From 4ff2d40654d9383f18482336a5c7ae151b1a12f4 Mon Sep 17 00:00:00 2001 From: Bhushan Khope Date: Thu, 26 Sep 2024 16:27:05 -0400 Subject: [PATCH 1/7] create error indicator in design system --- libs/design-system/error-indicator/README.md | 3 ++ .../error-indicator/ng-package.json | 5 +++ .../error-indicator/src/index.ts | 1 + .../src/lib/error-indicator.component.html | 6 ++++ .../src/lib/error-indicator.component.scss | 17 ++++++++++ .../src/lib/error-indicator.component.spec.ts | 15 +++++++++ .../lib/error-indicator.component.stories.ts | 33 +++++++++++++++++++ .../src/lib/error-indicator.component.ts | 17 ++++++++++ tsconfig.base.json | 3 ++ 9 files changed, 100 insertions(+) create mode 100644 libs/design-system/error-indicator/README.md create mode 100644 libs/design-system/error-indicator/ng-package.json create mode 100644 libs/design-system/error-indicator/src/index.ts create mode 100644 libs/design-system/error-indicator/src/lib/error-indicator.component.html create mode 100644 libs/design-system/error-indicator/src/lib/error-indicator.component.scss create mode 100644 libs/design-system/error-indicator/src/lib/error-indicator.component.spec.ts create mode 100644 libs/design-system/error-indicator/src/lib/error-indicator.component.stories.ts create mode 100644 libs/design-system/error-indicator/src/lib/error-indicator.component.ts diff --git a/libs/design-system/error-indicator/README.md b/libs/design-system/error-indicator/README.md new file mode 100644 index 000000000..ebdb5ad05 --- /dev/null +++ b/libs/design-system/error-indicator/README.md @@ -0,0 +1,3 @@ +# @hra-ui/design-system/error-indicator + +Secondary entry point of `@hra-ui/design-system`. It can be used by importing from `@hra-ui/design-system/error-indicator`. diff --git a/libs/design-system/error-indicator/ng-package.json b/libs/design-system/error-indicator/ng-package.json new file mode 100644 index 000000000..c781f0df4 --- /dev/null +++ b/libs/design-system/error-indicator/ng-package.json @@ -0,0 +1,5 @@ +{ + "lib": { + "entryFile": "src/index.ts" + } +} diff --git a/libs/design-system/error-indicator/src/index.ts b/libs/design-system/error-indicator/src/index.ts new file mode 100644 index 000000000..4e7c37667 --- /dev/null +++ b/libs/design-system/error-indicator/src/index.ts @@ -0,0 +1 @@ +export * from './lib/error-indicator.component'; diff --git a/libs/design-system/error-indicator/src/lib/error-indicator.component.html b/libs/design-system/error-indicator/src/lib/error-indicator.component.html new file mode 100644 index 000000000..e12d533c4 --- /dev/null +++ b/libs/design-system/error-indicator/src/lib/error-indicator.component.html @@ -0,0 +1,6 @@ +error +
+ @for (error of errors(); track error) { +
{{ error }}
+ } +
diff --git a/libs/design-system/error-indicator/src/lib/error-indicator.component.scss b/libs/design-system/error-indicator/src/lib/error-indicator.component.scss new file mode 100644 index 000000000..26c8beed3 --- /dev/null +++ b/libs/design-system/error-indicator/src/lib/error-indicator.component.scss @@ -0,0 +1,17 @@ +:host { + display: flex; + width: fit-content; + padding: 0.5rem 0.75rem; + border-radius: 0.25rem; + color: var(--sys-on-error-container); + background-color: color-mix(in srgb, var(--sys-error-container) 80%, transparent); + + mat-icon { + margin-right: 0.5rem; + color: var(--sys-error); + } + .error { + font: var(--sys-label-large); + letter-spacing: var(--sys-label-large-text-tracking); + } +} diff --git a/libs/design-system/error-indicator/src/lib/error-indicator.component.spec.ts b/libs/design-system/error-indicator/src/lib/error-indicator.component.spec.ts new file mode 100644 index 000000000..4af763010 --- /dev/null +++ b/libs/design-system/error-indicator/src/lib/error-indicator.component.spec.ts @@ -0,0 +1,15 @@ +import { render } from '@testing-library/angular'; +import { ErrorIndicatorComponent } from './error-indicator.component'; +import { screen } from '@testing-library/dom'; +describe('ErrorIndicatorComponent', () => { + it('Error should be visible in the indicator', async () => { + const errors: string[] = ['Error 1']; + await render(ErrorIndicatorComponent, { + componentInputs: { + errors, + }, + }); + + expect(screen.getByText('Error 1')).toBeInTheDocument(); + }); +}); diff --git a/libs/design-system/error-indicator/src/lib/error-indicator.component.stories.ts b/libs/design-system/error-indicator/src/lib/error-indicator.component.stories.ts new file mode 100644 index 000000000..67cfedb0c --- /dev/null +++ b/libs/design-system/error-indicator/src/lib/error-indicator.component.stories.ts @@ -0,0 +1,33 @@ +import { applicationConfig, Meta, StoryObj } from '@storybook/angular'; +import { provideDesignSystem } from '../../../src'; +import { ErrorIndicatorComponent } from './error-indicator.component'; + +const meta: Meta = { + component: ErrorIndicatorComponent, + title: 'ErrorIndicatorComponent', + parameters: { + design: { + type: 'figma', + url: 'https://www.figma.com/design/BCEJn9KCIbBJ5MzqnojKQp/Design-System-Components?node-id=5-842', + }, + }, + decorators: [ + applicationConfig({ + providers: [provideDesignSystem()], + }), + ], +}; +export default meta; +type Story = StoryObj; + +export const SingleError: Story = { + args: { + errors: ['Please upload a dataset.'], + }, +}; + +export const MultipleErrors: Story = { + args: { + errors: ['Required columns missing: Column Name, Column Name', 'Please upload a file with all required columns.'], + }, +}; diff --git a/libs/design-system/error-indicator/src/lib/error-indicator.component.ts b/libs/design-system/error-indicator/src/lib/error-indicator.component.ts new file mode 100644 index 000000000..ecf42bf41 --- /dev/null +++ b/libs/design-system/error-indicator/src/lib/error-indicator.component.ts @@ -0,0 +1,17 @@ +import { ChangeDetectionStrategy, Component, input } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { MatIconModule } from '@angular/material/icon'; + +/** Error Indicator component */ +@Component({ + selector: 'hra-error-indicator', + standalone: true, + imports: [CommonModule, MatIconModule], + templateUrl: './error-indicator.component.html', + styleUrl: './error-indicator.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ErrorIndicatorComponent { + /** List of errors to be shown in the indicator */ + readonly errors = input(); +} diff --git a/tsconfig.base.json b/tsconfig.base.json index 0995373b6..ef6854d28 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -89,6 +89,9 @@ "@hra-ui/design-system/dialog": [ "libs/design-system/dialog/src/index.ts" ], + "@hra-ui/design-system/error-indicator": [ + "libs/design-system/error-indicator/src/index.ts" + ], "@hra-ui/design-system/footer": [ "libs/design-system/footer/src/index.ts" ], From 4a422844bd78e45e31d5b13691d340eaef39c302 Mon Sep 17 00:00:00 2001 From: Bhushan Khope Date: Fri, 27 Sep 2024 10:48:39 -0400 Subject: [PATCH 2/7] update design url in story --- .../src/lib/error-indicator.component.stories.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/design-system/error-indicator/src/lib/error-indicator.component.stories.ts b/libs/design-system/error-indicator/src/lib/error-indicator.component.stories.ts index 67cfedb0c..60cc1b6fa 100644 --- a/libs/design-system/error-indicator/src/lib/error-indicator.component.stories.ts +++ b/libs/design-system/error-indicator/src/lib/error-indicator.component.stories.ts @@ -8,7 +8,7 @@ const meta: Meta = { parameters: { design: { type: 'figma', - url: 'https://www.figma.com/design/BCEJn9KCIbBJ5MzqnojKQp/Design-System-Components?node-id=5-842', + url: 'https://www.figma.com/design/BCEJn9KCIbBJ5MzqnojKQp/Explorer-Components?node-id=1294-4977', }, }, decorators: [ From 82000fc4ebfb5fe154c12158ce1a4f3501b2df56 Mon Sep 17 00:00:00 2001 From: Bhushan Khope <53601863+bhushankhope@users.noreply.github.com> Date: Fri, 27 Sep 2024 15:18:24 -0400 Subject: [PATCH 3/7] Body UI wrapper (#740) * create body ui wrapper * added outputs to body ui * improve test coverage * add new inputs to body ui * refactor body ui library * improve test coverage for body ui library * add transform for bounds input * Update app.component.ts Remove console.log * add redirects file * added preview config in project.json * update base href * update main path in project.json * update static file path * update project config * restore wrong config changes * add base href to staging, prod and dev * Update styles.scss --------- Co-authored-by: Daniel Bolin --- apps/body-ui-e2e/.eslintrc.json | 10 + apps/body-ui-e2e/cypress.config.ts | 7 + apps/body-ui-e2e/project.json | 29 +++ apps/body-ui-e2e/src/e2e/app.cy.ts | 13 ++ apps/body-ui-e2e/src/fixtures/example.json | 5 + apps/body-ui-e2e/src/support/app.po.ts | 1 + apps/body-ui-e2e/src/support/commands.ts | 35 +++ apps/body-ui-e2e/src/support/e2e.ts | 17 ++ apps/body-ui-e2e/tsconfig.json | 17 ++ apps/body-ui/.eslintrc.json | 33 +++ apps/body-ui/jest.config.ts | 22 ++ apps/body-ui/project.json | 104 +++++++++ apps/body-ui/public/favicon.ico | Bin 0 -> 15086 bytes apps/body-ui/src/_redirects | 1 + apps/body-ui/src/index.html | 15 ++ apps/body-ui/src/main.ts | 8 + apps/body-ui/src/styles.scss | 5 + apps/body-ui/src/test-setup.ts | 8 + apps/body-ui/tsconfig.app.json | 10 + apps/body-ui/tsconfig.editor.json | 6 + apps/body-ui/tsconfig.json | 32 +++ apps/body-ui/tsconfig.spec.json | 11 + libs/ccf-body-ui/.eslintrc.json | 4 +- libs/ccf-body-ui/jest.config.ts | 22 ++ libs/ccf-body-ui/karma.conf.js | 43 ---- libs/ccf-body-ui/project.json | 9 +- libs/ccf-body-ui/src/ccf-body-ui.spec.ts | 6 - .../src/lib/body-ui/body-ui.component.html | 1 + .../src/lib/body-ui/body-ui.component.scss | 0 .../src/lib/body-ui/body-ui.component.spec.ts | 134 +++++++++++ .../src/lib/body-ui/body-ui.component.ts | 220 ++++++++++++++++++ libs/ccf-body-ui/src/public-api.ts | 1 + libs/ccf-body-ui/src/test-setup.ts | 9 + libs/ccf-body-ui/src/test.ts | 11 - libs/ccf-body-ui/tsconfig.spec.json | 8 +- nx.json | 5 + 36 files changed, 791 insertions(+), 71 deletions(-) create mode 100644 apps/body-ui-e2e/.eslintrc.json create mode 100644 apps/body-ui-e2e/cypress.config.ts create mode 100644 apps/body-ui-e2e/project.json create mode 100644 apps/body-ui-e2e/src/e2e/app.cy.ts create mode 100644 apps/body-ui-e2e/src/fixtures/example.json create mode 100644 apps/body-ui-e2e/src/support/app.po.ts create mode 100644 apps/body-ui-e2e/src/support/commands.ts create mode 100644 apps/body-ui-e2e/src/support/e2e.ts create mode 100644 apps/body-ui-e2e/tsconfig.json create mode 100644 apps/body-ui/.eslintrc.json create mode 100644 apps/body-ui/jest.config.ts create mode 100644 apps/body-ui/project.json create mode 100644 apps/body-ui/public/favicon.ico create mode 100644 apps/body-ui/src/_redirects create mode 100644 apps/body-ui/src/index.html create mode 100644 apps/body-ui/src/main.ts create mode 100644 apps/body-ui/src/styles.scss create mode 100644 apps/body-ui/src/test-setup.ts create mode 100644 apps/body-ui/tsconfig.app.json create mode 100644 apps/body-ui/tsconfig.editor.json create mode 100644 apps/body-ui/tsconfig.json create mode 100644 apps/body-ui/tsconfig.spec.json create mode 100644 libs/ccf-body-ui/jest.config.ts delete mode 100644 libs/ccf-body-ui/karma.conf.js delete mode 100644 libs/ccf-body-ui/src/ccf-body-ui.spec.ts create mode 100644 libs/ccf-body-ui/src/lib/body-ui/body-ui.component.html create mode 100644 libs/ccf-body-ui/src/lib/body-ui/body-ui.component.scss create mode 100644 libs/ccf-body-ui/src/lib/body-ui/body-ui.component.spec.ts create mode 100644 libs/ccf-body-ui/src/lib/body-ui/body-ui.component.ts create mode 100644 libs/ccf-body-ui/src/test-setup.ts delete mode 100644 libs/ccf-body-ui/src/test.ts diff --git a/apps/body-ui-e2e/.eslintrc.json b/apps/body-ui-e2e/.eslintrc.json new file mode 100644 index 000000000..696cb8b12 --- /dev/null +++ b/apps/body-ui-e2e/.eslintrc.json @@ -0,0 +1,10 @@ +{ + "extends": ["plugin:cypress/recommended", "../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/apps/body-ui-e2e/cypress.config.ts b/apps/body-ui-e2e/cypress.config.ts new file mode 100644 index 000000000..7df58bdcf --- /dev/null +++ b/apps/body-ui-e2e/cypress.config.ts @@ -0,0 +1,7 @@ +import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset'; + +import { defineConfig } from 'cypress'; + +export default defineConfig({ + e2e: { ...nxE2EPreset(__filename, { cypressDir: 'src' }), baseUrl: 'http://localhost:4200' }, +}); diff --git a/apps/body-ui-e2e/project.json b/apps/body-ui-e2e/project.json new file mode 100644 index 000000000..e356206f1 --- /dev/null +++ b/apps/body-ui-e2e/project.json @@ -0,0 +1,29 @@ +{ + "name": "body-ui-e2e", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "projectType": "application", + "sourceRoot": "apps/body-ui-e2e/src", + "tags": [], + "implicitDependencies": ["body-ui"], + "targets": { + "e2e": { + "executor": "@nx/cypress:cypress", + "options": { + "cypressConfig": "apps/body-ui-e2e/cypress.config.ts", + "testingType": "e2e", + "devServerTarget": "body-ui:serve:development" + }, + "configurations": { + "production": { + "devServerTarget": "body-ui:serve:production" + }, + "ci": { + "devServerTarget": "body-ui:serve-static" + } + } + }, + "lint": { + "executor": "@nx/eslint:lint" + } + } +} diff --git a/apps/body-ui-e2e/src/e2e/app.cy.ts b/apps/body-ui-e2e/src/e2e/app.cy.ts new file mode 100644 index 000000000..a2f8ad732 --- /dev/null +++ b/apps/body-ui-e2e/src/e2e/app.cy.ts @@ -0,0 +1,13 @@ +import { getGreeting } from '../support/app.po'; + +describe('body-ui-e2e', () => { + beforeEach(() => cy.visit('/')); + + it('should display welcome message', () => { + // Custom command example, see `../support/commands.ts` file + cy.login('my-email@something.com', 'myPassword'); + + // Function helper example, see `../support/app.po.ts` file + getGreeting().contains(/Welcome/); + }); +}); diff --git a/apps/body-ui-e2e/src/fixtures/example.json b/apps/body-ui-e2e/src/fixtures/example.json new file mode 100644 index 000000000..02e425437 --- /dev/null +++ b/apps/body-ui-e2e/src/fixtures/example.json @@ -0,0 +1,5 @@ +{ + "name": "Using fixtures to represent data", + "email": "hello@cypress.io", + "body": "Fixtures are a great way to mock data for responses to routes" +} diff --git a/apps/body-ui-e2e/src/support/app.po.ts b/apps/body-ui-e2e/src/support/app.po.ts new file mode 100644 index 000000000..329342469 --- /dev/null +++ b/apps/body-ui-e2e/src/support/app.po.ts @@ -0,0 +1 @@ +export const getGreeting = () => cy.get('h1'); diff --git a/apps/body-ui-e2e/src/support/commands.ts b/apps/body-ui-e2e/src/support/commands.ts new file mode 100644 index 000000000..c421a3c47 --- /dev/null +++ b/apps/body-ui-e2e/src/support/commands.ts @@ -0,0 +1,35 @@ +/// + +// *********************************************** +// This example commands.ts shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** + +// eslint-disable-next-line @typescript-eslint/no-namespace +declare namespace Cypress { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + interface Chainable { + login(email: string, password: string): void; + } +} + +// -- This is a parent command -- +Cypress.Commands.add('login', (email, password) => { + console.log('Custom command example: Login', email, password); +}); +// +// -- This is a child command -- +// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) +// +// +// -- This is a dual command -- +// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) +// +// +// -- This will overwrite an existing command -- +// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) diff --git a/apps/body-ui-e2e/src/support/e2e.ts b/apps/body-ui-e2e/src/support/e2e.ts new file mode 100644 index 000000000..1c1a9e772 --- /dev/null +++ b/apps/body-ui-e2e/src/support/e2e.ts @@ -0,0 +1,17 @@ +// *********************************************************** +// This example support/e2e.ts is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.ts using ES2015 syntax: +import './commands'; diff --git a/apps/body-ui-e2e/tsconfig.json b/apps/body-ui-e2e/tsconfig.json new file mode 100644 index 000000000..e28de1d79 --- /dev/null +++ b/apps/body-ui-e2e/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "allowJs": true, + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["cypress", "node"], + "sourceMap": false, + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["**/*.ts", "**/*.js", "cypress.config.ts", "**/*.cy.ts", "**/*.cy.js", "**/*.d.ts"] +} diff --git a/apps/body-ui/.eslintrc.json b/apps/body-ui/.eslintrc.json new file mode 100644 index 000000000..437641b9a --- /dev/null +++ b/apps/body-ui/.eslintrc.json @@ -0,0 +1,33 @@ +{ + "extends": ["../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts"], + "extends": ["plugin:@nx/angular", "plugin:@angular-eslint/template/process-inline-templates"], + "rules": { + "@angular-eslint/directive-selector": [ + "error", + { + "type": "attribute", + "prefix": "app", + "style": "camelCase" + } + ], + "@angular-eslint/component-selector": [ + "error", + { + "type": "element", + "prefix": "app", + "style": "kebab-case" + } + ] + } + }, + { + "files": ["*.html"], + "extends": ["plugin:@nx/angular-template"], + "rules": {} + } + ] +} diff --git a/apps/body-ui/jest.config.ts b/apps/body-ui/jest.config.ts new file mode 100644 index 000000000..5ed277347 --- /dev/null +++ b/apps/body-ui/jest.config.ts @@ -0,0 +1,22 @@ +/* eslint-disable */ +export default { + displayName: 'body-ui', + preset: '../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + coverageDirectory: '../../coverage/apps/body-ui', + transform: { + '^.+\\.(ts|mjs|js|html)$': [ + 'jest-preset-angular', + { + tsconfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + }, + ], + }, + transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], + snapshotSerializers: [ + 'jest-preset-angular/build/serializers/no-ng-attributes', + 'jest-preset-angular/build/serializers/ng-snapshot', + 'jest-preset-angular/build/serializers/html-comment', + ], +}; diff --git a/apps/body-ui/project.json b/apps/body-ui/project.json new file mode 100644 index 000000000..1409258f4 --- /dev/null +++ b/apps/body-ui/project.json @@ -0,0 +1,104 @@ +{ + "name": "body-ui", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "projectType": "application", + "prefix": "app", + "sourceRoot": "apps/body-ui/src", + "tags": ["type:app"], + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser-esbuild", + "outputs": ["{options.outputPath}"], + "options": { + "buildOptimizer": false, + "outputPath": "dist/apps/body-ui", + "index": "apps/body-ui/src/index.html", + "main": "apps/body-ui/src/main.ts", + "polyfills": ["zone.js"], + "tsConfig": "apps/body-ui/tsconfig.app.json", + "inlineStyleLanguage": "scss", + "assets": [ + { + "glob": "**/*", + "input": "apps/body-ui/public" + } + ], + "styles": ["apps/body-ui/src/styles.scss"], + "scripts": [] + }, + "configurations": { + "production": { + "baseHref": "/ui/body-ui/", + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kb", + "maximumError": "1mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "2kb", + "maximumError": "4kb" + } + ], + "outputHashing": "all" + }, + "development": { + "baseHref": "/", + "optimization": false, + "extractLicenses": false, + "sourceMap": true + }, + "staging": { + "baseHref": "/ui--staging/body-ui/", + "outputHashing": "none" + }, + "preview": { + "baseHref": "/apps/body-ui/", + "optimization": false, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "buildTarget": "body-ui:build:production" + }, + "development": { + "buildTarget": "body-ui:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "executor": "@angular-devkit/build-angular:extract-i18n", + "options": { + "buildTarget": "body-ui:build" + } + }, + "lint": { + "executor": "@nx/eslint:lint" + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "apps/body-ui/jest.config.ts" + } + }, + "serve-static": { + "executor": "@nx/web:file-server", + "options": { + "buildTarget": "body-ui:build", + "port": 4200, + "staticFilePath": "dist/apps/body-ui/browser", + "spa": true + } + } + } +} diff --git a/apps/body-ui/public/favicon.ico b/apps/body-ui/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..317ebcb2336e0833a22dddf0ab287849f26fda57 GIT binary patch literal 15086 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 + + + + Body UI + + + + + + + diff --git a/apps/body-ui/src/main.ts b/apps/body-ui/src/main.ts new file mode 100644 index 000000000..e7312ed31 --- /dev/null +++ b/apps/body-ui/src/main.ts @@ -0,0 +1,8 @@ +import { provideHttpClient } from '@angular/common/http'; +import { provideZoneChangeDetection } from '@angular/core'; +import { createCustomElement } from '@hra-ui/webcomponents'; +import { BodyUiComponent } from 'ccf-body-ui'; + +createCustomElement('hra-body-ui', BodyUiComponent, { + providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideHttpClient()], +}); diff --git a/apps/body-ui/src/styles.scss b/apps/body-ui/src/styles.scss new file mode 100644 index 000000000..c40bb56ae --- /dev/null +++ b/apps/body-ui/src/styles.scss @@ -0,0 +1,5 @@ +/* You can add global styles to this file, and also import other style files */ + +body { + margin: 0; +} diff --git a/apps/body-ui/src/test-setup.ts b/apps/body-ui/src/test-setup.ts new file mode 100644 index 000000000..ab1eeeb33 --- /dev/null +++ b/apps/body-ui/src/test-setup.ts @@ -0,0 +1,8 @@ +// @ts-expect-error https://thymikee.github.io/jest-preset-angular/docs/getting-started/test-environment +globalThis.ngJest = { + testEnvironmentOptions: { + errorOnUnknownElements: true, + errorOnUnknownProperties: true, + }, +}; +import 'jest-preset-angular/setup-jest'; diff --git a/apps/body-ui/tsconfig.app.json b/apps/body-ui/tsconfig.app.json new file mode 100644 index 000000000..fff4a41d4 --- /dev/null +++ b/apps/body-ui/tsconfig.app.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "types": [] + }, + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts"], + "exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"] +} diff --git a/apps/body-ui/tsconfig.editor.json b/apps/body-ui/tsconfig.editor.json new file mode 100644 index 000000000..a8ac182c0 --- /dev/null +++ b/apps/body-ui/tsconfig.editor.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig.json", + "include": ["src/**/*.ts"], + "compilerOptions": {}, + "exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"] +} diff --git a/apps/body-ui/tsconfig.json b/apps/body-ui/tsconfig.json new file mode 100644 index 000000000..2f7d4b099 --- /dev/null +++ b/apps/body-ui/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "target": "es2022", + "forceConsistentCasingInFileNames": true, + "useDefineForClassFields": false, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.editor.json" + }, + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../tsconfig.base.json", + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/apps/body-ui/tsconfig.spec.json b/apps/body-ui/tsconfig.spec.json new file mode 100644 index 000000000..2269b492d --- /dev/null +++ b/apps/body-ui/tsconfig.spec.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "target": "es2016", + "types": ["jest", "node", "@testing-library/jest-dom"] + }, + "files": ["src/test-setup.ts"], + "include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"] +} diff --git a/libs/ccf-body-ui/.eslintrc.json b/libs/ccf-body-ui/.eslintrc.json index 285db045e..46454ca9d 100644 --- a/libs/ccf-body-ui/.eslintrc.json +++ b/libs/ccf-body-ui/.eslintrc.json @@ -9,7 +9,7 @@ "error", { "type": "element", - "prefix": "ccf", + "prefix": "hra", "style": "kebab-case" } ], @@ -17,7 +17,7 @@ "error", { "type": "attribute", - "prefix": "ccf", + "prefix": "hra", "style": "camelCase" } ] diff --git a/libs/ccf-body-ui/jest.config.ts b/libs/ccf-body-ui/jest.config.ts new file mode 100644 index 000000000..9eef19b82 --- /dev/null +++ b/libs/ccf-body-ui/jest.config.ts @@ -0,0 +1,22 @@ +/* eslint-disable */ +export default { + displayName: 'ccf-body-ui', + preset: '../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts', '@testing-library/jest-dom'], + coverageDirectory: '../../coverage/libs/ccf-body-ui', + transform: { + '^.+\\.(ts|mjs|js|html)$': [ + 'jest-preset-angular', + { + tsconfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + }, + ], + }, + transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], + snapshotSerializers: [ + 'jest-preset-angular/build/serializers/no-ng-attributes', + 'jest-preset-angular/build/serializers/ng-snapshot', + 'jest-preset-angular/build/serializers/html-comment', + ], +}; diff --git a/libs/ccf-body-ui/karma.conf.js b/libs/ccf-body-ui/karma.conf.js deleted file mode 100644 index 7b27003bf..000000000 --- a/libs/ccf-body-ui/karma.conf.js +++ /dev/null @@ -1,43 +0,0 @@ -// Karma configuration file, see link for more information -// https://karma-runner.github.io/1.0/config/configuration-file.html - -module.exports = function (config) { - config.set({ - basePath: '', - frameworks: ['jasmine', '@angular-devkit/build-angular'], - plugins: [ - require('karma-jasmine'), - require('karma-chrome-launcher'), - require('karma-jasmine-html-reporter'), - require('karma-coverage-istanbul-reporter'), - require('@angular-devkit/build-angular/plugins/karma'), - ], - client: { - clearContext: false, // leave Jasmine Spec Runner output visible in browser - }, - coverageIstanbulReporter: { - dir: require('path').join(__dirname, '../../coverage/ccf-body-ui'), - reports: ['html', 'lcovonly', 'text-summary'], - fixWebpackSourcePaths: true, - thresholds: { - emitWarning: false, - global: { - lines: 20, - }, - }, - }, - reporters: ['progress', 'kjhtml', 'coverage-istanbul'], - port: 9876, - colors: true, - logLevel: config.LOG_INFO, - browsers: ['ChromeHeadless' + (require('is-wsl') ? 'WSL' : '')], - singleRun: true, - - customLaunchers: { - ChromeHeadlessWSL: { - base: 'ChromeHeadless', - flags: ['--no-sandbox', '--disable-features=VizDisplayCompositor'], - }, - }, - }); -}; diff --git a/libs/ccf-body-ui/project.json b/libs/ccf-body-ui/project.json index be30474a9..0351b5d89 100644 --- a/libs/ccf-body-ui/project.json +++ b/libs/ccf-body-ui/project.json @@ -19,13 +19,10 @@ } }, "test": { - "executor": "@angular-devkit/build-angular:karma", + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], "options": { - "main": "libs/ccf-body-ui/src/test.ts", - "tsConfig": "libs/ccf-body-ui/tsconfig.spec.json", - "karmaConfig": "libs/ccf-body-ui/karma.conf.js", - "codeCoverage": true, - "codeCoverageExclude": ["**/src/test.ts", "**/*.module.ts"] + "jestConfig": "libs/ccf-body-ui/jest.config.ts" } }, "lint": { diff --git a/libs/ccf-body-ui/src/ccf-body-ui.spec.ts b/libs/ccf-body-ui/src/ccf-body-ui.spec.ts deleted file mode 100644 index 7c114a027..000000000 --- a/libs/ccf-body-ui/src/ccf-body-ui.spec.ts +++ /dev/null @@ -1,6 +0,0 @@ -describe('ccf-body-ui', () => { - // Temporary noop test - it('passes', () => { - expect(true).toBeTruthy(); - }); -}); diff --git a/libs/ccf-body-ui/src/lib/body-ui/body-ui.component.html b/libs/ccf-body-ui/src/lib/body-ui/body-ui.component.html new file mode 100644 index 000000000..d03820a75 --- /dev/null +++ b/libs/ccf-body-ui/src/lib/body-ui/body-ui.component.html @@ -0,0 +1 @@ + diff --git a/libs/ccf-body-ui/src/lib/body-ui/body-ui.component.scss b/libs/ccf-body-ui/src/lib/body-ui/body-ui.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/libs/ccf-body-ui/src/lib/body-ui/body-ui.component.spec.ts b/libs/ccf-body-ui/src/lib/body-ui/body-ui.component.spec.ts new file mode 100644 index 000000000..3f758c940 --- /dev/null +++ b/libs/ccf-body-ui/src/lib/body-ui/body-ui.component.spec.ts @@ -0,0 +1,134 @@ +import { provideHttpClient } from '@angular/common/http'; +import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; +import { ErrorHandler } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { SpatialSceneNode } from '@hra-api/ng-client'; +import { render, screen } from '@testing-library/angular'; +import { BodyUI, NodeClickEvent } from '../body-ui'; +import { BodyUiComponent, XYZTriplet } from './body-ui.component'; +import { Subject } from 'rxjs'; + +jest.mock('../body-ui.ts', () => ({ + BodyUI: jest.fn().mockImplementation(() => ({ + deck: { + setProps: jest.fn(), + finalize: jest.fn(), + }, + initialize: jest.fn().mockResolvedValue(undefined), + finalize: jest.fn(), + setScene: jest.fn(), + setZoom: jest.fn(), + nodeClick$: new Subject(), + nodeHoverStart$: new Subject(), + nodeHoverStop$: new Subject(), + sceneRotation$: new Subject(), + nodeDrag$: new Subject(), + })), +})); + +describe('BodyUiComponent', () => { + const sampleSpatialSceneNode: SpatialSceneNode = {}; + const providers = [provideHttpClient(), provideHttpClientTesting()]; + + function getBodyUi(): BodyUI { + return jest.mocked(BodyUI).mock.results[0].value; + } + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should render a canvas element', async () => { + await render(BodyUiComponent, { providers }); + + const canvasElement = screen.getByRole('img'); + + expect(canvasElement).toBeTruthy(); + expect(canvasElement.tagName.toLowerCase()).toBe('canvas'); + }); + + it('should set zoom according to the bounds', async () => { + const bounds: XYZTriplet = { x: 0, y: 0, z: 0 }; + const { + fixture: { componentInstance: component }, + } = await render(BodyUiComponent, { + providers, + inputs: { + bounds: bounds, + }, + }); + + component.zoomToBounds(bounds); + expect(getBodyUi().setZoom).toHaveBeenCalled(); + }); + + it('it should return array as it is when input is not string', async () => { + const scenes: SpatialSceneNode[] = []; + const { fixture } = await render(BodyUiComponent, { + providers, + inputs: { + scene: scenes, + }, + }); + + await fixture.whenStable(); + expect(getBodyUi().setScene).toHaveBeenCalledWith(scenes); + }); + + it('it fetch the scene data and return when a url is passed as scene input', async () => { + const URL = 'https://example.com'; + const { fixture } = await render(BodyUiComponent, { + providers, + inputs: { + scene: URL, + }, + }); + + const controller = TestBed.inject(HttpTestingController); + const req = controller.expectOne(URL); + req.flush([sampleSpatialSceneNode]); + + await fixture.whenStable(); + expect(getBodyUi().setScene).toHaveBeenCalledWith([sampleSpatialSceneNode]); + }); + + it('it should throw error if response is of wrong type', async () => { + const URL = 'https://example.com'; + const errorHandler = { handleError: jest.fn() }; + const { fixture } = await render(BodyUiComponent, { + providers: [...providers, { provide: ErrorHandler, useValue: errorHandler }], + inputs: { + scene: URL, + }, + }); + + const controller = TestBed.inject(HttpTestingController); + const req = controller.expectOne(URL); + req.flush('Error Data'); + + await fixture.whenStable(); + expect(getBodyUi().setScene).not.toHaveBeenCalledWith([sampleSpatialSceneNode]); + expect(errorHandler.handleError).toHaveBeenCalled(); + }); + + it('outputs should emit value', async () => { + const event: NodeClickEvent = { + node: { + name: 'test', + }, + ctrlClick: true, + }; + const nodeClick = jest.fn(); + const { fixture } = await render(BodyUiComponent, { + providers, + on: { + nodeClick, + }, + }); + await fixture.whenStable(); + + const nodeClick$ = getBodyUi().nodeClick$ as Subject; + nodeClick$.next(event); + expect(nodeClick).toHaveBeenCalledWith(event); + }); +}); diff --git a/libs/ccf-body-ui/src/lib/body-ui/body-ui.component.ts b/libs/ccf-body-ui/src/lib/body-ui/body-ui.component.ts new file mode 100644 index 000000000..7e37328e4 --- /dev/null +++ b/libs/ccf-body-ui/src/lib/body-ui/body-ui.component.ts @@ -0,0 +1,220 @@ +import { HttpClient } from '@angular/common/http'; +import { + booleanAttribute, + Component, + computed, + effect, + ElementRef, + ErrorHandler, + inject, + input, + numberAttribute, + output, + OutputEmitterRef, + Signal, + viewChild, +} from '@angular/core'; +import { SpatialSceneNode } from '@hra-api/ng-client'; +import { derivedAsync } from 'ngxtension/derived-async'; +import { catchError, map, Observable, of, Subscription } from 'rxjs'; +import { z } from 'zod'; +import { BodyUI, NodeClickEvent, NodeDragEvent } from '../body-ui'; + +/** Interface for bounds */ +export interface XYZTriplet { + x: T; + y: T; + z: T; +} + +/** Type for setter methods of the BodyUI */ +type SetterMethods = keyof { + [P in keyof BodyUI as BodyUI[P] extends (v: ValueT) => void ? P : never]: BodyUI[P]; +}; + +/** Parses the input if it is a JSON String */ +function tryParseJson(value: unknown): unknown { + try { + if (typeof value === 'string') { + return JSON.parse(value); + } + } catch { + // Ignore errors + } + + return value; +} + +/** Utility function to use input data to set to relevant body ui setter */ +function connectInput( + bodyUi: Signal, + source: Signal, + setter: SetterMethods, +): void { + const value = source(); + if (value !== undefined) { + bodyUi()?.[setter](value as never); + } +} + +/** Utility function to use output data to set to relevant body ui subject */ +function connectOutput(out: OutputEmitterRef, source: Observable): Subscription { + return source.subscribe((value) => out.emit(value)); +} + +/** Zod for SPATIAL SCENE NODE */ +export const SPATIAL_SCENE_NODE = z + .object({}) + .passthrough() + .refine((obj): obj is SpatialSceneNode => true); + +/** Zod for SPATIAL SCENE NODE array */ +export const SPATIAL_SCENE_NODE_ARRAY = z.array(SPATIAL_SCENE_NODE); + +/** Preprocesses the scene input */ +const SCENE_INPUT = z.preprocess(tryParseJson, z.union([z.literal(''), z.string().url(), SPATIAL_SCENE_NODE_ARRAY])); +/** Bind scene input */ +const parseSceneInput = SCENE_INPUT.parse.bind(SCENE_INPUT); + +/** Preprocess the target input */ +const TARGET_INPUT = z.preprocess(tryParseJson, z.tuple([z.number(), z.number(), z.number()])); +/** Bind target input */ +const parseTargetInput = TARGET_INPUT.parse.bind(TARGET_INPUT); + +/** Preprocess the bounds input */ +const BOUNDS_INPUT = z.preprocess(tryParseJson, z.object({ x: z.number(), y: z.number(), z: z.number() })); + +/** Bind the bounds input */ +const parseBoundsInput = BOUNDS_INPUT.parse.bind(BOUNDS_INPUT); + +/** HRA Body UI Component */ +@Component({ + standalone: true, + imports: [], + selector: 'hra-body-ui', + templateUrl: './body-ui.component.html', + styleUrl: './body-ui.component.scss', +}) +export class BodyUiComponent { + /** Scene for the deck gl */ + readonly scene = input(undefined, { transform: parseSceneInput }); + + /** Rotation for the deck gl */ + readonly rotation = input(undefined, { transform: numberAttribute }); + /** Rotation X for the deck gl */ + readonly rotationX = input(undefined, { transform: numberAttribute }); + /** Zoom for the deck gl */ + readonly zoom = input(undefined, { transform: numberAttribute }); + /** Target for the deck gl */ + readonly target = input(undefined, { transform: parseTargetInput }); + /** Bounds for the deck gl */ + readonly bounds = input(undefined, { transform: parseBoundsInput }); + /** Camera for the deck gl */ + readonly camera = input(); + /** Flag for the interactive for deck gl */ + readonly interactive = input(true, { transform: booleanAttribute }); + + /** Output for rotation change */ + readonly rotationChange = output<[number, number]>(); + /** Output for node click */ + readonly nodeClick = output(); + /** Output for node drag */ + readonly nodeDrag = output(); + /** Output for node hover start */ + readonly nodeHoverStart = output(); + /** Output for node hover end */ + readonly nodeHoverStop = output(); + + /** Instance of HttpClient */ + private readonly http = inject(HttpClient); + /** Instance of Error Handler */ + private readonly errorHandler = inject(ErrorHandler); + + /** Instance of canvas element */ + private readonly canvas = viewChild.required>('canvas'); + /** Instance of BodyUI class */ + private readonly bodyUi = derivedAsync( + async () => { + const bodyUi = new BodyUI({ + id: 'bodyUI', + canvas: this.canvas().nativeElement, + zoom: 9.5, + target: [0, 0, 0], + rotation: 0, + minRotationX: -75, + maxRotationX: 75, + interactive: true, + camera: '', + }); + + await bodyUi.initialize(); + return bodyUi; + }, + { initialValue: undefined }, + ); + + /** Processed scene data for deck gl */ + private readonly sceneData = derivedAsync( + () => { + const value = this.scene(); + if (!value) { + return []; + } else if (typeof value !== 'string') { + return value; + } + + return this.http.get(value).pipe( + map((data) => SPATIAL_SCENE_NODE_ARRAY.parse(data)), + catchError((error) => { + this.errorHandler.handleError(error); + return of([]); + }), + ); + }, + { initialValue: [] }, + ); + + /** Returns the bounds zoom according to bounds input */ + private readonly boundsZoom = computed(() => { + const bounds = this.bounds(); + return bounds ? this.getBoundsZoom(bounds) : undefined; + }); + + /** Constructor for the component */ + constructor() { + effect(() => connectInput(this.bodyUi, this.sceneData, 'setScene')); + effect(() => connectInput(this.bodyUi, this.rotation, 'setRotation')); + effect(() => connectInput(this.bodyUi, this.rotationX, 'setRotationX')); + effect(() => connectInput(this.bodyUi, this.target, 'setTarget')); + effect(() => connectInput(this.bodyUi, this.boundsZoom, 'setZoom')); + + effect((onCleanup) => { + const bodyUi = this.bodyUi(); + if (bodyUi) { + const subscriptions = new Subscription(); + subscriptions.add(connectOutput(this.nodeClick, bodyUi.nodeClick$)); + subscriptions.add(connectOutput(this.nodeDrag, bodyUi.nodeDrag$)); + subscriptions.add(connectOutput(this.nodeHoverStart, bodyUi.nodeHoverStart$)); + subscriptions.add(connectOutput(this.nodeHoverStop, bodyUi.nodeHoverStop$)); + subscriptions.add(() => bodyUi.finalize()); + onCleanup(() => subscriptions.unsubscribe()); + } + }); + } + + /** Sets the deck gl zoom according to the provided bounds */ + zoomToBounds(bounds: XYZTriplet, margin = { x: 48, y: 48 }): void { + const zoom = this.getBoundsZoom(bounds, margin); + this.bodyUi()?.setZoom(zoom); + } + + /** Returns zoom value according to current bounds */ + private getBoundsZoom(bounds: XYZTriplet, margin = { x: 48, y: 48 }): number { + const { width, height } = this.canvas().nativeElement; + const pxRatio = window.devicePixelRatio; + return Math.min( + Math.log2((width - margin.x) / pxRatio / bounds.x), + Math.log2((height - margin.y) / pxRatio / bounds.y), + ); + } +} diff --git a/libs/ccf-body-ui/src/public-api.ts b/libs/ccf-body-ui/src/public-api.ts index f7d97d9e0..1af3a8201 100644 --- a/libs/ccf-body-ui/src/public-api.ts +++ b/libs/ccf-body-ui/src/public-api.ts @@ -7,6 +7,7 @@ */ export * from './lib/body-ui-layer'; export * from './lib/body-ui'; +export * from './lib/body-ui/body-ui.component'; export * from './lib/shared/spatial-scene-node'; export * from './lib/shared/ccf-spatial-jsonld'; diff --git a/libs/ccf-body-ui/src/test-setup.ts b/libs/ccf-body-ui/src/test-setup.ts new file mode 100644 index 000000000..82cfe9b70 --- /dev/null +++ b/libs/ccf-body-ui/src/test-setup.ts @@ -0,0 +1,9 @@ +// @ts-expect-error https://thymikee.github.io/jest-preset-angular/docs/getting-started/test-environment +globalThis.ngJest = { + testEnvironmentOptions: { + errorOnUnknownElements: true, + errorOnUnknownProperties: true, + }, +}; +import 'jest-preset-angular/setup-jest'; +import '@testing-library/jest-dom'; diff --git a/libs/ccf-body-ui/src/test.ts b/libs/ccf-body-ui/src/test.ts deleted file mode 100644 index e4e6ef81e..000000000 --- a/libs/ccf-body-ui/src/test.ts +++ /dev/null @@ -1,11 +0,0 @@ -// This file is required by karma.conf.js and loads recursively all the .spec and framework files - -import 'zone.js'; -import 'zone.js/testing'; -import { getTestBed } from '@angular/core/testing'; -import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; - -// First, initialize the Angular testing environment. -getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), { - teardown: { destroyAfterEach: false }, -}); diff --git a/libs/ccf-body-ui/tsconfig.spec.json b/libs/ccf-body-ui/tsconfig.spec.json index 5b3e61112..7e19a99c6 100644 --- a/libs/ccf-body-ui/tsconfig.spec.json +++ b/libs/ccf-body-ui/tsconfig.spec.json @@ -2,8 +2,10 @@ "extends": "./tsconfig.json", "compilerOptions": { "outDir": "../../dist/out-tsc", - "types": ["jasmine", "node"] + "module": "commonjs", + "target": "es2016", + "types": ["jest", "node", "@testing-library/jest-dom"] }, - "files": ["src/test.ts"], - "include": ["**/*.spec.ts", "**/*.d.ts"] + "files": ["src/test-setup.ts"], + "include": ["jest.config.ts", "**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"] } diff --git a/nx.json b/nx.json index 4d9ba0ef6..2327a2234 100644 --- a/nx.json +++ b/nx.json @@ -100,6 +100,11 @@ "cache": true, "dependsOn": ["^build"], "inputs": ["production", "^production"] + }, + "@angular-devkit/build-angular:application": { + "cache": true, + "dependsOn": ["^build"], + "inputs": ["production", "^production"] } }, "generators": { From 946d2a5d8ff798f646c6b5f4dcb0e7b09e67e2d6 Mon Sep 17 00:00:00 2001 From: Daniel Bolin Date: Mon, 30 Sep 2024 13:58:03 -0400 Subject: [PATCH 4/7] build(body-ui): :construction_worker: Update build executor (#747) --- .vscode/settings.json | 3 ++- apps/body-ui/project.json | 31 ++++++++++++++++++------------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index e3a41041c..8a2aa6c80 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,6 +11,7 @@ "ftu-ui", "services", "ccf-organ-info", - "design-system" + "design-system", + "body-ui" ] } diff --git a/apps/body-ui/project.json b/apps/body-ui/project.json index 1409258f4..af9ade3f9 100644 --- a/apps/body-ui/project.json +++ b/apps/body-ui/project.json @@ -7,13 +7,15 @@ "tags": ["type:app"], "targets": { "build": { - "executor": "@angular-devkit/build-angular:browser-esbuild", - "outputs": ["{options.outputPath}"], + "executor": "@nx/angular:application", + "outputs": ["{options.outputPath.base}"], "options": { - "buildOptimizer": false, - "outputPath": "dist/apps/body-ui", + "outputPath": { + "base": "dist/apps/body-ui", + "browser": "." + }, "index": "apps/body-ui/src/index.html", - "main": "apps/body-ui/src/main.ts", + "browser": "apps/body-ui/src/main.ts", "polyfills": ["zone.js"], "tsConfig": "apps/body-ui/tsconfig.app.json", "inlineStyleLanguage": "scss", @@ -24,7 +26,12 @@ } ], "styles": ["apps/body-ui/src/styles.scss"], - "scripts": [] + "scripts": [], + "define": { + "define": "undefined" + }, + "outputHashing": "none", + "allowedCommonJsDependencies": ["*"] }, "configurations": { "production": { @@ -32,16 +39,15 @@ "budgets": [ { "type": "initial", - "maximumWarning": "500kb", - "maximumError": "1mb" + "maximumWarning": "1mb", + "maximumError": "2mb" }, { "type": "anyComponentStyle", "maximumWarning": "2kb", "maximumError": "4kb" } - ], - "outputHashing": "all" + ] }, "development": { "baseHref": "/", @@ -50,8 +56,7 @@ "sourceMap": true }, "staging": { - "baseHref": "/ui--staging/body-ui/", - "outputHashing": "none" + "baseHref": "/ui--staging/body-ui/" }, "preview": { "baseHref": "/apps/body-ui/", @@ -64,7 +69,7 @@ "defaultConfiguration": "production" }, "serve": { - "executor": "@angular-devkit/build-angular:dev-server", + "executor": "@nx/angular:dev-server", "configurations": { "production": { "buildTarget": "body-ui:build:production" From 5f5899d4072fc547d0e66a38c8862bf3df8a44f5 Mon Sep 17 00:00:00 2001 From: Daniel Bolin Date: Mon, 30 Sep 2024 15:05:55 -0400 Subject: [PATCH 5/7] refactor(body-ui): Tweak body ui styles --- libs/ccf-body-ui/src/lib/body-ui/body-ui.component.scss | 3 +++ libs/ccf-body-ui/src/lib/body-ui/body-ui.component.ts | 2 ++ 2 files changed, 5 insertions(+) diff --git a/libs/ccf-body-ui/src/lib/body-ui/body-ui.component.scss b/libs/ccf-body-ui/src/lib/body-ui/body-ui.component.scss index e69de29bb..5d4e87f30 100644 --- a/libs/ccf-body-ui/src/lib/body-ui/body-ui.component.scss +++ b/libs/ccf-body-ui/src/lib/body-ui/body-ui.component.scss @@ -0,0 +1,3 @@ +:host { + display: block; +} diff --git a/libs/ccf-body-ui/src/lib/body-ui/body-ui.component.ts b/libs/ccf-body-ui/src/lib/body-ui/body-ui.component.ts index 7e37328e4..ba7d68585 100644 --- a/libs/ccf-body-ui/src/lib/body-ui/body-ui.component.ts +++ b/libs/ccf-body-ui/src/lib/body-ui/body-ui.component.ts @@ -13,6 +13,7 @@ import { OutputEmitterRef, Signal, viewChild, + ViewEncapsulation, } from '@angular/core'; import { SpatialSceneNode } from '@hra-api/ng-client'; import { derivedAsync } from 'ngxtension/derived-async'; @@ -94,6 +95,7 @@ const parseBoundsInput = BOUNDS_INPUT.parse.bind(BOUNDS_INPUT); selector: 'hra-body-ui', templateUrl: './body-ui.component.html', styleUrl: './body-ui.component.scss', + encapsulation: ViewEncapsulation.ShadowDom, }) export class BodyUiComponent { /** Scene for the deck gl */ From 55a66309711c38c3f94838805352231f447b4d6c Mon Sep 17 00:00:00 2001 From: Bhushan Khope <53601863+bhushankhope@users.noreply.github.com> Date: Mon, 30 Sep 2024 15:06:20 -0400 Subject: [PATCH 6/7] create menu size directive (#749) --- .../src/lib/directives/menu-size.directive.ts | 36 ++++++++++++++++ .../lib/menu-demo/menu-demo.component.html | 4 +- .../src/lib/menu-demo/menu-demo.component.ts | 3 +- .../menu-styles/menu-styles.component.scss | 41 ++++++++++--------- 4 files changed, 61 insertions(+), 23 deletions(-) create mode 100644 libs/design-system/menu/src/lib/directives/menu-size.directive.ts diff --git a/libs/design-system/menu/src/lib/directives/menu-size.directive.ts b/libs/design-system/menu/src/lib/directives/menu-size.directive.ts new file mode 100644 index 000000000..828cc1041 --- /dev/null +++ b/libs/design-system/menu/src/lib/directives/menu-size.directive.ts @@ -0,0 +1,36 @@ +import { Directive, effect, inject, input } from '@angular/core'; +import { MatMenu } from '@angular/material/menu'; + +/** Type for Menu Sizes */ +export type MenuSize = 'small' | 'medium' | 'large'; + +/** Menu Size Directive */ +@Directive({ + selector: '[hraMenuSize]', + standalone: true, +}) +export class MenuSizeDirective { + /** Size of menu to use */ + readonly size = input('medium', { alias: 'hraMenuSize' }); + + /** Instance of Mat Menu */ + protected readonly menu = inject(MatMenu); + + /** Cleanup the previous panel class and assign the current */ + constructor() { + effect((onCleanup) => { + const { menu } = this; + const cls = ` ${this.size()}`; + menu.panelClass = this.getPanelClass() + cls; + + onCleanup(() => (menu.panelClass = this.getPanelClass().replace(cls, ''))); + }); + } + + /** Utility function to get the panel class because they dont have the getter for panelClass in mat menu */ + private getPanelClass(): string { + // WARNING: Pulling from internals since panelClass doesn't have a getter. + // May break in the future! + return (this.menu as unknown as { _previousPanelClass: string })._previousPanelClass; + } +} diff --git a/libs/design-system/menu/src/lib/menu-demo/menu-demo.component.html b/libs/design-system/menu/src/lib/menu-demo/menu-demo.component.html index 6fa777c8c..e2390144f 100644 --- a/libs/design-system/menu/src/lib/menu-demo/menu-demo.component.html +++ b/libs/design-system/menu/src/lib/menu-demo/menu-demo.component.html @@ -7,7 +7,7 @@ more_vert - + @for (option of menuOptions(); track option) { @if (option.expandedOptions) {