Adds test harnesses for tenant isolation, TypeORM/MikroORM parity, persistence invariants and idempotency; startup validation for DB config; Nx plugin/core boundary rules; a fresh-database migration smoke test; and a correlation id carried from HTTP requests into docs queue jobs and logs. Testing (packages/core/src/lib/core/testing): - tenant-isolation: an in-memory tenant-aware repository, fixtures and strict assertions (a non-empty page, every row in the caller's tenant). Applied to EmployeeService and OrganizationProjectService. - orm-conformance and persistence-invariants: suites that run the same checks under TypeORM and MikroORM (run-both-orms.sh uses yarn nx). - idempotency: assertion helpers and specs for token cleanup (positive control), employee notifications and the Zapier timer webhook. - database/migration-smoke.spec.ts: runs the full migration chain on a fresh better-sqlite3 database. It is excluded from the default core jest run; run it with `nx run core:test-migration-smoke`. Idempotency: - EmployeeNotificationService.create() takes an opt-in `absorbRedelivery`. With it set, an identical unread, unarchived notification under 60 s old (same receiver, entity, type, sender, title and message) is returned instead of inserted, and a missing key or a failed lookup inserts as usual. The event handler does not enable it (the in-process EventBus never redelivers), so every caller inserts one row per event as before. - ZapierWebhookService skips resending a webhook that already succeeded for the same subscription, action and time log within 5 minutes. The key is reserved while in flight, failed deliveries stay retryable, pruning stops at the first unexpired entry, and the cache is capped at 10,000 entries. Config (packages/config): - An unknown DB_TYPE fails fast with the list of supported values; an empty DB_TYPE still means better-sqlite3; mongodb throws an Error. - Pool and timeout variables are parsed with Number.parseInt semantics, only for postgres and mysql. They throw only where tarn already refused to start and warn otherwise. SQLite ignores them and still prints the startup values. Observability: - RequestContextMiddleware accepts an inbound x-correlation-id of 1-128 visible ASCII characters (otherwise it generates a UUIDv4) and echoes it on the response. CORS allows and exposes X-Correlation-Id. RequestContext.currentCorrelationId() is added. - The docs queue carries the correlation id through job payloads (including bulk reindex) into pipeline outcome, error, dead-letter and enqueue-failure logs. Boundaries and build: - Every project gets a type tag. The ESLint depConstraints stop type:core depending on plugins, and plugins may depend only on core, shared libs and the known extension points (ai-chat, job-proposal, integration-ai, job-*-ui). - core declares @gauzy/scheduler (package.json and implicitDependencies); the webapp Dockerfile copies scheduler's package.json. - The integration-zapier jest config can import @gauzy/core (transformIgnorePatterns, allowJs, isolatedModules). - Fixes the stale @nrwl/nx eslint-disable id in the e2e roles-permissions steps and the broken @gauzy/core mock in the docs document-scope spec. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@gauzy/ui-react
React-to-Angular bridge for Gauzy UI plugins. Renders React 19 components inside Angular with full access to Angular services via the dependency injection system.
Overview
This package provides the glue layer between Angular and React. It is intentionally separate from @gauzy/plugin-ui (the core plugin infrastructure) so that plugin-ui remains a pure-Angular package with no React peer dependency.
@gauzy/plugin-ui (core, Angular-only)
│
└── @gauzy/ui-react (React bridge)
│
└── @gauzy/plugin-dashboard-time-track-react-ui (example React dashboard plugin)
Installation
yarn add @gauzy/ui-react
Peer Dependencies
@angular/core≥ 21@gauzy/plugin-ui(workspace peer)react≥ 19react-dom≥ 19rxjs≥ 7
Angular Directives
ReactHostDirective
Renders a React component synchronously inside any Angular template. Selector: [gaReactHost].
<!-- host element receives the React tree -->
<div [gaReactHost]="MyReactComponent" [props]="myProps"></div>
<!-- with extra context values (a component field, see below) -->
<div [gaReactHost]="MyReactComponent" [props]="myProps" [context]="hostContext"></div>
// Both bound objects are built ONCE — never as inline literals in the template.
readonly hostContext = { theme: 'dark' };
myProps: MyProps = {};
ngOnInit(): void {
// `@ViewChild(..., { static: true })` references exist here, not at field-initializer time.
this.myProps = { headerActionsHost: this.headerActions.nativeElement };
}
Build the props and context objects once (e.g. as class fields or in ngOnInit), not inline in the template — the directive re-renders the React root whenever the props / context reference changes, so a fresh object literal per change-detection pass re-renders on every tick.
LazyReactHostDirective
Lazy-loads a React component via dynamic import() for code-splitting. Selector: [gaReactLazyHost].
<div [gaReactLazyHost]="loadKanban" [props]="kanbanProps"></div>
loadKanban = () => import('./kanban/KanbanBoard').then((m) => m);
React Hooks
useInjector()
Access Angular services from inside any React component rendered via the bridge.
import { useInjector } from '@gauzy/ui-react';
import { Router } from '@angular/router';
function MyWidget() {
const router = useInjector(Router);
return <button onClick={() => router.navigate(['/home'])}>Go Home</button>;
}
Without a token, returns the raw Injector:
const injector = useInjector();
const http = injector.get(HttpClient);
usePluginEvents(pluginId?)
Inter-plugin event bus from React. Subscribe and emit events across plugins.
import { usePluginEvents } from '@gauzy/ui-react';
function MyWidget() {
const events = usePluginEvents('my-plugin');
useEffect(() => {
const sub = events.on('time-tracked').subscribe((event) => {
console.log('Payload:', event.payload);
});
return () => sub.unsubscribe();
}, [events]);
return <button onClick={() => events.emit('widget-clicked', { id: 1 })}>Click</button>;
}
usePluginEvent(type, callback, options?, deps?)
Single-event subscription with automatic cleanup on unmount.
import { usePluginEvent } from '@gauzy/ui-react';
function MyWidget() {
usePluginEvent('time-tracked', (event) => {
console.log('Time tracked:', event.payload);
});
return <div>Listening...</div>;
}
useBridgeContext()
Access the full bridge context (injector + any extra values passed via the context input).
import { useBridgeContext } from '@gauzy/ui-react';
function MyWidget() {
const { injector, theme } = useBridgeContext();
return <div style={{ color: theme as string }}>Hello</div>;
}
React Bridge API
ReactBridge
Low-level class implementing UiBridge from @gauzy/plugin-ui. Use it when you need direct control over mounting/unmounting.
import { ReactBridge } from '@gauzy/ui-react';
const bridge = new ReactBridge();
const result = bridge.mount({
component: MyReactComponent,
props: { title: 'Hello' },
hostElement: document.getElementById('host')!,
injector: angularInjector
});
// Later:
result.unmount();
// Or update props:
result.updateProps?.({ title: 'Updated' });
provideReactBridge()
Environment provider — registers ReactBridge in Angular DI.
import { provideReactBridge, REACT_BRIDGE } from '@gauzy/ui-react';
// In app.config.ts or AppModule providers:
provideReactBridge();
// Then inject anywhere:
const bridge = inject(REACT_BRIDGE);
Defining React Extensions
Use defineReactExtension() to register a React component as a plugin extension slot contribution.
import { defineReactExtension } from '@gauzy/ui-react';
import { MyWidget } from './MyWidget';
export const myExtension = defineReactExtension({
id: 'my-widget',
slotId: 'dashboard-widgets',
component: MyWidget,
props: { title: 'My Widget' },
order: 10
});
Then add it to a plugin definition:
import { defineDeclarativePlugin } from '@gauzy/plugin-ui';
export const MyPlugin = defineDeclarativePlugin('my-plugin', {
extensions: [myExtension]
});
Building
yarn nx build ui-react
yarn nx build ui-react --configuration=production
Testing
yarn nx test ui-react