diff --git a/packages/react-ui/Home.js b/packages/react-ui/Home.js new file mode 100644 index 0000000..cc830b2 --- /dev/null +++ b/packages/react-ui/Home.js @@ -0,0 +1,78 @@ +import React from 'react'; + +export default class Home extends React.Component { + constructor(props) { + super(props); + } + + render() { + const reactVersion = require('./package.json').dependencies['react']; + + const styles = { + container: { + fontFamily: 'Arial, sans-serif', + padding: '20px', + backgroundColor: '#f0f2f5', + borderRadius: '8px', + boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)', + maxWidth: '600px', + margin: '20px auto', + border: '1px solid #ddd' + }, + header: { + color: '#1a1a1a', + textAlign: 'center', + borderBottom: '2px solid #e0e0e0', + paddingBottom: '15px', + marginBottom: '20px', + fontSize: '24px' + }, + content: { + color: '#333', + }, + paragraph: { + lineHeight: '1.6', + fontSize: '16px', + marginBottom: '15px' + }, + box: { + backgroundColor: '#ffffff', + border: '1px solid #e0e0e0', + padding: '15px', + borderRadius: '6px', + margin: '20px 0', + boxShadow: '0 1px 2px rgba(0, 0, 0, 0.05)' + }, + code: { + fontFamily: 'monospace', + backgroundColor: '#e8e8e8', + padding: '2px 4px', + borderRadius: '3px', + }, + strong: { + color: '#0056b3' + } + }; + + return ( +
+

React Application as a Web Component

+
+

+ This user interface is built using React. +

+
+

React Version: {reactVersion}

+
+

+ The entire React application is encapsulated and delivered as a Web Component. + This is a standards-based way to create custom, reusable HTML elements that can be used with any JavaScript library or framework, or even with no framework at all. +

+

+ This approach promotes interoperability and allows different parts of a larger application to be built with different technologies without conflicts. +

+
+
+ ); + } +} diff --git a/packages/react-ui/app.js b/packages/react-ui/app.js new file mode 100644 index 0000000..d07312d --- /dev/null +++ b/packages/react-ui/app.js @@ -0,0 +1,46 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { BrowserRouter, MemoryRouter, Switch, Route } from 'react-router-dom'; + +import Home from './Home'; + +function App({ baseHref }) { + // When the host app uses hash-based routing (e.g. VCFA tenant), Angular owns + // the hash so React cannot use BrowserRouter (pathname mismatch) or HashRouter + // (it would read Angular's hash segment). MemoryRouter is used instead. + const isHashBased = baseHref && !window.location.pathname.startsWith(baseHref); + const Router = isHashBased ? MemoryRouter : BrowserRouter; + const routerProps = isHashBased ? { initialEntries: ['/'] } : { basename: baseHref }; + + return ( + +
+
+ + + +
+
+
+ ); +} + +class MyReactAppUIElement extends HTMLElement { + connectedCallback() { + const baseHref = this.getAttribute('baseHref'); + ReactDOM.render(, this); + } + + static get observedAttributes() { + return ['baseHref']; + } + + attributeChangedCallback(name, oldValue, newValue) { + if (name === 'baseHref') { + console.log(`baseHref changed from ${oldValue} to ${newValue}`); + } + } + +} + +customElements.define('react-element', MyReactAppUIElement); diff --git a/packages/react-ui/index.html b/packages/react-ui/index.html new file mode 100644 index 0000000..b5858e2 --- /dev/null +++ b/packages/react-ui/index.html @@ -0,0 +1,2 @@ + + diff --git a/packages/react-ui/index.js b/packages/react-ui/index.js new file mode 100644 index 0000000..b5fb900 --- /dev/null +++ b/packages/react-ui/index.js @@ -0,0 +1 @@ +import('./app'); diff --git a/packages/react-ui/package.json b/packages/react-ui/package.json new file mode 100644 index 0000000..46d97a5 --- /dev/null +++ b/packages/react-ui/package.json @@ -0,0 +1,30 @@ +{ + "name": "react-ui", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "start": "webpack serve --config webpack.config.js --mode=development --devtool=source-map", + "build": "webpack --config webpack.config.js --mode=development", + "build:plugin": "webpack --config webpack.config.plugin.js --mode=development" + }, + "author": "", + "license": "MIT", + "devDependencies": { + "@babel/core": "^7.12.10", + "@babel/preset-env": "^7.12.10", + "@babel/preset-react": "^7.12.10", + "babel-runtime": "^6.26.0", + "babel-loader": "^8.2.2", + "babel-plugin-transform-runtime": "^6.23.0", + "webpack": "^5.10.1", + "webpack-cli": "^4.2.0", + "webpack-dev-server": "^3.11.0" + }, + "dependencies": { + "copy-webpack-plugin": "^8.1.1", + "react": "^17.0.1", + "react-dom": "^17.0.1", + "react-router-dom": "5.3.3" + } +} diff --git a/packages/react-ui/plugin.js b/packages/react-ui/plugin.js new file mode 100644 index 0000000..b5fb900 --- /dev/null +++ b/packages/react-ui/plugin.js @@ -0,0 +1 @@ +import('./app'); diff --git a/packages/react-ui/webpack.config.js b/packages/react-ui/webpack.config.js new file mode 100644 index 0000000..fa7e1ab --- /dev/null +++ b/packages/react-ui/webpack.config.js @@ -0,0 +1,53 @@ +const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin"); +const CopyWebpackPlugin = require("copy-webpack-plugin"); +const path = require("path"); + +module.exports = options => { + return { + entry: './index.js', + output: { + filename: 'bundle.js', + publicPath: "auto", + uniqueName: "react-example-ui", + path: path.resolve(__dirname, './dist'), + }, + module: { + rules: [ + { + test: /.js$/, + exclude: /node_modules/, + use: [ + { + loader: 'babel-loader', + options: { + cacheDirectory: true, + presets: ['@babel/react', '@babel/env'] + } + }, + ], + }, + ], + }, + plugins: [ + new ModuleFederationPlugin({ + name: "react_example_ui", + library: { type: "var", name: "react_example_ui" }, + filename: "remoteEntry.js", + exposes: { + './web-components': './app.js', + }, + shared: ["react", "react-dom"] + }), + new CopyWebpackPlugin({ + patterns: [ + { + from: './*.html' + } + ] + }) + ], + devServer: { + port: 4204 + } + } +} diff --git a/packages/react-ui/webpack.config.plugin.js b/packages/react-ui/webpack.config.plugin.js new file mode 100644 index 0000000..014845e --- /dev/null +++ b/packages/react-ui/webpack.config.plugin.js @@ -0,0 +1,53 @@ +const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin"); +const CopyWebpackPlugin = require("copy-webpack-plugin"); +const path = require("path"); + +module.exports = options => { + return { + entry: './plugin.js', + output: { + filename: 'bundle.js', + publicPath: "auto", + uniqueName: "react-example-ui", + path: path.resolve(__dirname, '../vcfa-plugin/projects/ui-plugin/public/assets/react-plugin/dist'), + }, + module: { + rules: [ + { + test: /.js$/, + exclude: /node_modules/, + use: [ + { + loader: 'babel-loader', + options: { + cacheDirectory: true, + presets: ['@babel/react', '@babel/env'] + } + }, + ], + }, + ], + }, + plugins: [ + new ModuleFederationPlugin({ + name: "react_example_ui", + library: { type: "var", name: "react_example_ui" }, + filename: "remoteEntry.js", + exposes: { + './web-components': './app.js', + }, + shared: ["react", "react-dom"] + }), + new CopyWebpackPlugin({ + patterns: [ + { + from: './*.html' + } + ] + }) + ], + devServer: { + port: 4204 + } + } +} diff --git a/packages/vcfa-plugin/README.md b/packages/vcfa-plugin/README.md new file mode 100644 index 0000000..83e4949 --- /dev/null +++ b/packages/vcfa-plugin/README.md @@ -0,0 +1,30 @@ +# Prerequisites +- NodeJS >= v22.14.x + +# Start +``` +nvm use 22 +npm ci +npm run start +``` + +# Build +``` +nvm use 22 +npm ci +npm run build +``` + +# Test +``` +nvm use 22 +npm ci +npm run test +``` + +# Build VCFA Plugin +``` +nvm use 22 +npm ci +npm run build:plugin +``` \ No newline at end of file diff --git a/packages/vcfa-plugin/package.json b/packages/vcfa-plugin/package.json index 0a8f389..212f784 100644 --- a/packages/vcfa-plugin/package.json +++ b/packages/vcfa-plugin/package.json @@ -6,45 +6,50 @@ }, "scripts": { "ng": "ng", - "start": "ng serve", + "start": "cd ../react-ui && npm run build:plugin && cd ../vcfa-plugin && ng serve", "build": "ng build", - "build:plugin": "ng build --configuration plugin && npm run zip", + "build:plugin": "cd ../react-ui && npm run build:plugin && cd ../vcfa-plugin && ng build --configuration plugin && npm run zip", "test": "ng test", - "zip": "node ./scripts/zip.js ./dist/quick-start ./dist/plugin.zip" + "zip": "node ./scripts/zip.js ./dist/ui-plugin ./dist/plugin.zip" }, "private": true, "dependencies": { - "@angular-architects/module-federation": "19.0.3", - "@angular/animations": "19.2.0", - "@angular/cdk": "19.2.0", - "@angular/common": "19.2.0", - "@angular/compiler": "19.2.0", - "@angular/core": "19.2.0", - "@angular/forms": "19.2.0", - "@angular/platform-browser": "19.2.0", - "@angular/platform-browser-dynamic": "19.2.0", - "@angular/router": "19.2.0", - "@cds/core": "6.15.1", - "@clr/angular": "17.10.0", - "@clr/ui": "17.10.0", - "ansi-escapes": "7.1.0", - "rxjs": "7.8.0", - "tslib": "2.3.0", - "zone.js": "0.15.0" + "@angular-architects/module-federation": "21.2.2", + "@angular/animations": "21.2.17", + "@angular/cdk": "21.2.14", + "@angular/common": "21.2.17", + "@angular/compiler": "21.2.17", + "@angular/core": "21.2.17", + "@angular/forms": "21.2.17", + "@angular/platform-browser": "21.2.17", + "@angular/platform-browser-dynamic": "21.2.17", + "@angular/router": "21.2.17", + "@clr/angular": "18.2.1", + "@clr/ui": "18.2.1", + "@ngx-translate/core": "17.0.0", + "@ngx-translate/http-loader": "17.0.0", + "@vcd/bindings": "9.1.1", + "@vcfa/container-hooks": "19.0.4", + "@vcfa/sdk": "19.1.9", + "ansi-escapes": "7.3.0", + "rxjs": "7.8.2", + "tslib": "2.8.1", + "zone.js": "0.16.2" }, "devDependencies": { - "@angular-devkit/build-angular": "19.2.0", - "@angular/cli": "19.2.0", - "@angular/compiler-cli": "19.2.0", - "@types/jasmine": "5.1.0", + "@angular-devkit/build-angular": "21.2.17", + "@angular/build": "21.2.17", + "@angular/cli": "21.2.17", + "@angular/compiler-cli": "21.2.17", + "@types/jasmine": "5.1.15", "archiver": "7.0.1", - "jasmine-core": "5.6.0", - "karma": "6.4.0", + "jasmine-core": "5.13.0", + "karma": "6.4.4", "karma-chrome-launcher": "3.2.0", - "karma-coverage": "2.2.0", + "karma-coverage": "2.2.1", "karma-jasmine": "5.1.0", - "karma-jasmine-html-reporter": "2.1.0", - "ngx-build-plus": "19.0.0", - "typescript": "5.7.2" + "karma-jasmine-html-reporter": "2.2.0", + "ngx-build-plus": "20.0.0", + "typescript": "5.9.3" } } diff --git a/packages/vcfa-plugin/projects/ui-plugin/public/assets/i18n/en.json b/packages/vcfa-plugin/projects/ui-plugin/public/assets/i18n/en.json new file mode 100644 index 0000000..c184e35 --- /dev/null +++ b/packages/vcfa-plugin/projects/ui-plugin/public/assets/i18n/en.json @@ -0,0 +1,5 @@ +{ + "subnav.menu.home": "Home", + "home.title": "Welcome to your VCFA Plugin", + "home.description": "The Quick Start Plugin showcases all available extension points in VCFA where you can start building your own plugin." +} \ No newline at end of file diff --git a/packages/vcfa-plugin/projects/ui-plugin/public/assets/iframe-content.html b/packages/vcfa-plugin/projects/ui-plugin/public/assets/iframe-content.html new file mode 100644 index 0000000..374fe80 --- /dev/null +++ b/packages/vcfa-plugin/projects/ui-plugin/public/assets/iframe-content.html @@ -0,0 +1,42 @@ + + + + + + Iframe Content + + + +

Iframe Content Page

+

This page is designed to receive a JWT from a parent window via postMessage.

+

Received JWT:

+
Waiting for token...
+ + + + diff --git a/packages/vcfa-plugin/projects/ui-plugin/public/manifest.json b/packages/vcfa-plugin/projects/ui-plugin/public/manifest.json index ebc12c8..0a7008f 100644 --- a/packages/vcfa-plugin/projects/ui-plugin/public/manifest.json +++ b/packages/vcfa-plugin/projects/ui-plugin/public/manifest.json @@ -2,7 +2,7 @@ "urn": "vcfa:plugin:quick-start", "name": "Quick Start Plugin", "containerVersion": "9.1.0", - "manifestVersion": "6.0.0", + "manifestVersion": "7.0.0", "version": "0.0.0", "scope": [ "service-provider", diff --git a/packages/vcfa-plugin/projects/ui-plugin/src/app/app.routes.ts b/packages/vcfa-plugin/projects/ui-plugin/src/app/app.routes.ts index c12dd1b..92a412e 100644 --- a/packages/vcfa-plugin/projects/ui-plugin/src/app/app.routes.ts +++ b/packages/vcfa-plugin/projects/ui-plugin/src/app/app.routes.ts @@ -1,7 +1,11 @@ import { Routes } from "@angular/router"; import { HomeComponent } from "../plugin/home/home.component"; +import { JwtIframeComponent } from "../plugin/jwt-iframe/jwt-iframe.component"; +import { ReactAppComponent } from "../react/react-app.component"; export const routes: Routes = [ { path: "", redirectTo: "home", pathMatch: "full" }, { path: "home", component: HomeComponent }, -]; \ No newline at end of file + { path: "access-token", component: JwtIframeComponent }, + { path: "react-example", component: ReactAppComponent }, +]; diff --git a/packages/vcfa-plugin/projects/ui-plugin/src/bootstrap.plugin.ts b/packages/vcfa-plugin/projects/ui-plugin/src/bootstrap.plugin.ts index 032c4bc..d16b8c1 100644 --- a/packages/vcfa-plugin/projects/ui-plugin/src/bootstrap.plugin.ts +++ b/packages/vcfa-plugin/projects/ui-plugin/src/bootstrap.plugin.ts @@ -1,2 +1,20 @@ +import { provideHttpClient } from '@angular/common/http'; +import { provideTranslateService } from '@ngx-translate/core'; +import { TranslateLoader } from '@ngx-translate/core'; +import { HttpClient } from '@angular/common/http'; +import { DynamicTranslateLoader } from './plugin/dynamic-translate-loader'; +import { EXTENSION_ASSET_URL } from '@vcfa/sdk'; + export { SubnavComponent } from './plugin/subnav.component'; export { routes } from './plugin/plugin.routes'; +export const providers = [ + provideHttpClient(), + provideTranslateService({ + loader: { + provide: TranslateLoader, + useFactory: (http: HttpClient, assetUrl: string) => new DynamicTranslateLoader(http, `${assetUrl}/i18n/`), + deps: [HttpClient, EXTENSION_ASSET_URL], + }, + fallbackLang: 'en', + }), +]; \ No newline at end of file diff --git a/packages/vcfa-plugin/projects/ui-plugin/src/bootstrap.ts b/packages/vcfa-plugin/projects/ui-plugin/src/bootstrap.ts index 981801d..03c6112 100644 --- a/packages/vcfa-plugin/projects/ui-plugin/src/bootstrap.ts +++ b/packages/vcfa-plugin/projects/ui-plugin/src/bootstrap.ts @@ -1,11 +1,24 @@ import { bootstrapApplication } from '@angular/platform-browser'; +import { provideHttpClient } from '@angular/common/http'; import { provideRouter } from '@angular/router'; import { routes } from './app/app.routes'; import { AppComponent } from './app/app.component'; +import { provideTranslateService } from "@ngx-translate/core"; +import { provideTranslateHttpLoader } from "@ngx-translate/http-loader"; bootstrapApplication(AppComponent, { providers: [ provideRouter(routes), + provideHttpClient(), + provideTranslateService({ + loader: provideTranslateHttpLoader({ + prefix: '/assets/i18n/', + suffix: '.json' + }), + fallbackLang: 'en', + lang: 'en' + }) + ] }) .catch((err) => console.error(err)); diff --git a/packages/vcfa-plugin/projects/ui-plugin/src/plugin/dynamic-translate-loader.ts b/packages/vcfa-plugin/projects/ui-plugin/src/plugin/dynamic-translate-loader.ts new file mode 100644 index 0000000..e60c7ae --- /dev/null +++ b/packages/vcfa-plugin/projects/ui-plugin/src/plugin/dynamic-translate-loader.ts @@ -0,0 +1,20 @@ +import { HttpClient } from '@angular/common/http'; +import { TranslateLoader } from '@ngx-translate/core'; + +export class DynamicTranslateLoader implements TranslateLoader { + constructor( + private http: HttpClient, + private prefix: string = './assets/i18n/', + private suffix: string = '.json' + ) { } + + /** + * Gets the translations for a given language. + * @param lang The language to get the translations for. + * @returns An observable containing the loaded translations. + */ + public getTranslation(lang: string): any { + const fullPath = `${this.prefix}${lang}${this.suffix}`; + return this.http.get(fullPath); + } +} \ No newline at end of file diff --git a/packages/vcfa-plugin/projects/ui-plugin/src/plugin/home/home.component.html b/packages/vcfa-plugin/projects/ui-plugin/src/plugin/home/home.component.html index ebf108a..e03420d 100644 --- a/packages/vcfa-plugin/projects/ui-plugin/src/plugin/home/home.component.html +++ b/packages/vcfa-plugin/projects/ui-plugin/src/plugin/home/home.component.html @@ -1,6 +1,6 @@
👋
-

Welcome to your VCFA Plugin

-

The Quick Start Plugin showcases all available extension points in VCFA where you can start building your own plugin.

+

{{ 'home.title' | translate }}

+

{{ 'home.description' | translate }}

+ +
diff --git a/packages/vcfa-plugin/projects/ui-plugin/src/plugin/jwt-iframe/jwt-iframe.component.scss b/packages/vcfa-plugin/projects/ui-plugin/src/plugin/jwt-iframe/jwt-iframe.component.scss new file mode 100644 index 0000000..f83d4f0 --- /dev/null +++ b/packages/vcfa-plugin/projects/ui-plugin/src/plugin/jwt-iframe/jwt-iframe.component.scss @@ -0,0 +1,7 @@ +.iframe-container { + padding: 1rem; +} + +iframe { + border: 1px solid #ccc; +} diff --git a/packages/vcfa-plugin/projects/ui-plugin/src/plugin/jwt-iframe/jwt-iframe.component.ts b/packages/vcfa-plugin/projects/ui-plugin/src/plugin/jwt-iframe/jwt-iframe.component.ts new file mode 100644 index 0000000..12ea289 --- /dev/null +++ b/packages/vcfa-plugin/projects/ui-plugin/src/plugin/jwt-iframe/jwt-iframe.component.ts @@ -0,0 +1,72 @@ +import { CommonModule } from '@angular/common'; +import { Component, ElementRef, Inject, OnDestroy, OnInit, AfterViewInit, Optional, ViewChild } from '@angular/core'; +import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; +import { AuthTokenHolderService, EXTENSION_ASSET_URL } from '@vcfa/sdk'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; + +@Component({ + standalone: true, + imports: [CommonModule], + selector: 'app-jwt-iframe', + templateUrl: './jwt-iframe.component.html', + styleUrls: ['./jwt-iframe.component.scss'], +}) +export class JwtIframeComponent implements OnInit, AfterViewInit, OnDestroy { + @ViewChild('iframe', { static: true }) iframe!: ElementRef; + private destroy$ = new Subject(); + // A placeholder URL for the iframe content. + // In a real scenario, this would be the URL of the page that can receive the JWT. + iframeSrc: SafeResourceUrl = '/iframe-content.html'; + + constructor( + @Optional() @Inject(EXTENSION_ASSET_URL) public pluginAssetUrl: string, + @Optional() @Inject(AuthTokenHolderService) private authTokenHolderService: AuthTokenHolderService, + private sanitizer: DomSanitizer, + ) {} + + ngOnInit(): void { + this.iframeSrc = this.sanitizer.bypassSecurityTrustResourceUrl(`${this.pluginAssetUrl ? this.pluginAssetUrl : '/assets'}/iframe-content.html`); + } + + ngAfterViewInit(): void { + this.initTokenListener(); + window.addEventListener('message', this.handleIframeMessage.bind(this)); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + window.removeEventListener('message', this.handleIframeMessage.bind(this)); + } + + private handleIframeMessage(event: MessageEvent): void { + if (event.data && event.data.status === 'ready') { + this.initTokenListener(); + } + } + + private initTokenListener(): void { + if (this.authTokenHolderService) { + // Send the initial token + this.sendToken(this.authTokenHolderService.jwt); + + // Subscribe to token changes + if (this.authTokenHolderService.jwtAsync) { + this.authTokenHolderService.jwtAsync + .pipe(takeUntil(this.destroy$)) + .subscribe((token: string | undefined) => { + this.sendToken(token); + }); + } + } else { + this.sendToken(undefined); + } + } + + private sendToken(token: string | undefined): void { + if (this.iframe && this.iframe.nativeElement.contentWindow) { + this.iframe.nativeElement.contentWindow.postMessage(token, '*'); + } + } +} diff --git a/packages/vcfa-plugin/projects/ui-plugin/src/plugin/plugin.routes.ts b/packages/vcfa-plugin/projects/ui-plugin/src/plugin/plugin.routes.ts index 5ab4491..e3d63c6 100644 --- a/packages/vcfa-plugin/projects/ui-plugin/src/plugin/plugin.routes.ts +++ b/packages/vcfa-plugin/projects/ui-plugin/src/plugin/plugin.routes.ts @@ -1,7 +1,11 @@ import { Routes } from "@angular/router"; import { HomeComponent } from "./home/home.component"; +import { JwtIframeComponent } from "./jwt-iframe/jwt-iframe.component"; +import { ReactAppComponent } from "../react/react-app.component"; export const routes: Routes = [ { path: "", redirectTo: "home", pathMatch: "full" }, { path: "home", component: HomeComponent }, -]; \ No newline at end of file + { path: "react-example", component: ReactAppComponent }, + { path: "access-token", component: JwtIframeComponent }, +]; diff --git a/packages/vcfa-plugin/projects/ui-plugin/src/plugin/subnav.component.html b/packages/vcfa-plugin/projects/ui-plugin/src/plugin/subnav.component.html index 9bcba00..71fdc01 100755 --- a/packages/vcfa-plugin/projects/ui-plugin/src/plugin/subnav.component.html +++ b/packages/vcfa-plugin/projects/ui-plugin/src/plugin/subnav.component.html @@ -4,7 +4,7 @@ - + {{navItem.labelKey}} diff --git a/packages/vcfa-plugin/projects/ui-plugin/src/plugin/subnav.component.scss b/packages/vcfa-plugin/projects/ui-plugin/src/plugin/subnav.component.scss index 780c65a..cc31a6d 100644 --- a/packages/vcfa-plugin/projects/ui-plugin/src/plugin/subnav.component.scss +++ b/packages/vcfa-plugin/projects/ui-plugin/src/plugin/subnav.component.scss @@ -1,3 +1,3 @@ .main-content { - padding: 0 20px; + padding: 0 20px !important; } \ No newline at end of file diff --git a/packages/vcfa-plugin/projects/ui-plugin/src/plugin/subnav.component.ts b/packages/vcfa-plugin/projects/ui-plugin/src/plugin/subnav.component.ts index 42a89e9..aaf1984 100755 --- a/packages/vcfa-plugin/projects/ui-plugin/src/plugin/subnav.component.ts +++ b/packages/vcfa-plugin/projects/ui-plugin/src/plugin/subnav.component.ts @@ -4,9 +4,8 @@ import { ClarityModule } from "@clr/angular"; import { StandaloneHelperModule } from "./standalone/standalone.helper.module"; import { RouterLink, RouterLinkActive, RouterOutlet } from "@angular/router"; -import "@cds/core/icon/register"; -import { ClarityIcons, homeIcon } from "@cds/core/icon"; -ClarityIcons.addIcons(homeIcon); +import { ClarityIcons, homeIcon, cogIcon, atomIcon } from "@clr/angular"; +ClarityIcons.addIcons(homeIcon, cogIcon, atomIcon); @Component({ imports: [ @@ -20,10 +19,12 @@ ClarityIcons.addIcons(homeIcon); selector: "plugin-subnav", templateUrl: "./subnav.component.html", host: { "class": "content-container" }, - styleUrls: ["subnav.component.scss"], + styleUrls: ["./subnav.component.scss"], }) export class SubnavComponent { navItems: any[] = [ - { routerLink: "./home", iconShape: "home", labelKey: "Home" } + { routerLink: "./home", iconShape: "home", labelKey: "Home" }, + { routerLink: "./access-token", iconShape: "cog", labelKey: "Access Token" }, + { routerLink: "./react-example", iconShape: "atom", labelKey: "React Example" }, ]; } diff --git a/packages/vcfa-plugin/projects/ui-plugin/src/react/federation.ts b/packages/vcfa-plugin/projects/ui-plugin/src/react/federation.ts new file mode 100644 index 0000000..f896f11 --- /dev/null +++ b/packages/vcfa-plugin/projects/ui-plugin/src/react/federation.ts @@ -0,0 +1,12 @@ +import { init, ModuleFederation } from '@module-federation/enhanced/runtime'; + +export function createFederationInstance(): ModuleFederation { + // Store React dependencies in separate module federation instance. + const mf: ModuleFederation = init({ + name: 'my-react-plugin-host', + remotes: [], + shared: {} + }); + mf.initializeSharing(); + return mf; +} diff --git a/packages/vcfa-plugin/projects/ui-plugin/src/react/react-app.component.html b/packages/vcfa-plugin/projects/ui-plugin/src/react/react-app.component.html new file mode 100644 index 0000000..af2ebd8 --- /dev/null +++ b/packages/vcfa-plugin/projects/ui-plugin/src/react/react-app.component.html @@ -0,0 +1 @@ +
diff --git a/packages/vcfa-plugin/projects/ui-plugin/src/react/react-app.component.ts b/packages/vcfa-plugin/projects/ui-plugin/src/react/react-app.component.ts new file mode 100644 index 0000000..1307543 --- /dev/null +++ b/packages/vcfa-plugin/projects/ui-plugin/src/react/react-app.component.ts @@ -0,0 +1,64 @@ +import { CommonModule, NgComponentOutlet } from '@angular/common'; +import { Component, CUSTOM_ELEMENTS_SCHEMA, ElementRef, Inject, Optional, ViewChild } from '@angular/core'; +import { RouterOutlet } from '@angular/router'; +import { ClarityModule } from '@clr/angular'; +import { EXTENSION_ASSET_URL, EXTENSION_ROUTE } from '@vcfa/sdk'; + +import { createFederationInstance } from './federation'; + +@Component({ + selector: 'react-app', + imports: [ + RouterOutlet, + ClarityModule, + NgComponentOutlet, + CommonModule, + ], + templateUrl: './react-app.component.html', + schemas: [ + CUSTOM_ELEMENTS_SCHEMA + ], +}) +export class ReactAppComponent { + @ViewChild('reactApp', {static: true}) reactApp: ElementRef | undefined; + + private assetsUrl: string = '/assets'; + private baseHref: string = '/react-example'; + + constructor( + @Optional() @Inject(EXTENSION_ASSET_URL) private EXTENSION_ASSET_URL: string, + @Optional() @Inject(EXTENSION_ROUTE) private EXTENSION_ROUTE: string, + ) { + if (this.EXTENSION_ASSET_URL) { + this.assetsUrl = this.EXTENSION_ASSET_URL; + } + + if (this.EXTENSION_ROUTE) { + this.baseHref = `${this.EXTENSION_ROUTE}/react-example`; + } + + this.loadReactApplication(); + } + + private loadReactApplication() { + const mf = createFederationInstance(); + mf.registerRemotes([ + { + name: "react", + entry: `${this.assetsUrl}/react-plugin/dist/remoteEntry.js`, + type: "script", + entryGlobalName: "react_example_ui" + } + ]); + + mf.loadRemote("react/web-components").then((m) => { + const elementName = 'react-element'; + const reactApp = document.createElement(elementName); + reactApp.setAttribute("baseHref", this.baseHref); + this.reactApp?.nativeElement.appendChild(reactApp); + }).catch((e) => { + console.error("React Application failed to load", e); + this.reactApp?.nativeElement.appendChild(document.createTextNode("React Application failed to load...")); + }); + } +} diff --git a/packages/vcfa-plugin/projects/ui-plugin/src/styles.scss b/packages/vcfa-plugin/projects/ui-plugin/src/styles.scss index 2609775..6386d9c 100644 --- a/packages/vcfa-plugin/projects/ui-plugin/src/styles.scss +++ b/packages/vcfa-plugin/projects/ui-plugin/src/styles.scss @@ -1,4 +1,2 @@ /* You can add global styles to this file, and also import other style files */ -@import '@cds/core/global.min.css'; -@import '@cds/core/styles/theme.dark.min.css'; @import '@clr/ui/clr-ui.min.css'; \ No newline at end of file diff --git a/packages/vcfa-plugin/projects/ui-plugin/vcf-webpack-util.js b/packages/vcfa-plugin/projects/ui-plugin/vcf-webpack-util.js index b696241..f30cac6 100644 --- a/packages/vcfa-plugin/projects/ui-plugin/vcf-webpack-util.js +++ b/packages/vcfa-plugin/projects/ui-plugin/vcf-webpack-util.js @@ -21,9 +21,8 @@ function prepare() { const coreLibs = [ "@angular", "@clr", - "@cds", "rxjs", - "@vcfa" + "@vcfa/container-hooks" ]; const libsToShare = Object.keys(allRecords).map((npmPackageName) => {