The Unit Tests workflow has never been green (24 of 97 projects red at
develop eac7136562, run 36319732347). Causes addressed here:
- Env leak: Nx loads the committed .env.local (DEMO=true,
WORKER_QUEUE_ENABLED=false, NODE_ENV=development) into every task.
The test step now sets NX_LOAD_DOT_ENV_FILES=false, and the two specs
that depend on a mode pin it themselves (role-permission counts pin
environment.demo=false and gain a demo-mode suite asserting 209 per
role; the worker spec clears the queue env before the constants load).
.env.local is unchanged.
- Angular harness: nohoist gives each workspace its own @angular copy,
so setupZoneTestEnv() initialised a different TestBed from the one the
spec used. A workspace resolver (jest.resolver.js, wrapping Nx's) makes
every @angular/* import in a Jest project resolve to one copy. The
Angular projects now inherit the preset's transformIgnorePatterns,
which gains the .mjs exception plus @datorama, @ngneat and lodash-es.
- ui-config's environment.ts is generated and gitignored; the workflow
runs `yarn config:dev` before the tests, as the Playwright workflow does.
- Misconfigured targets: gauzy (jest.config.js -> .ts, Angular transform,
setupFile moved into the config), integration-sim-ui (.ts -> .cts),
integration-activepieces (config file added), mcp-auth (ran
`node build/main.js --test`; now the jest executor like apps/mcp).
- Real failures: the openai "silence" case used a body with no `text`,
which the shared helper rejects by contract; the toolbar spec read the
tabIndex property, which is 0 on any button; the docs-ui linearity
tests asserted wall-clock bounds, now a 4x-input growth ratio.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@gauzy/plugin-ui
Core UI plugin infrastructure for the Gauzy platform. Provides the types, lifecycle interfaces, Angular module, registries, and React bridge needed to build and register UI plugins.
Table of Contents
- Installation
- App-Level Setup
- Creating a Plugin
- Plugin Definition Reference
- Registering Plugins
- Lifecycle Hooks
- Declarative Registrations
- Activation Predicate (Feature Flags / Permissions)
- React Bridge
- Extension Slots
- Building
Installation
yarn add @gauzy/plugin-ui
# peer deps
yarn add @angular/core @angular/common react react-dom
App-Level Setup
Call PluginUiModule.init() once in your root bootstrap module. Pass the three registry services so that defineDeclarativePlugin can wire routes, tabs, and nav menu automatically.
// bootstrap.module.ts
import { NgModule } from '@angular/core';
import { PluginUiModule, PLUGIN_UI_CONFIG, getPluginUiConfig, setPluginUiConfig } from '@gauzy/plugin-ui';
import { NavMenuBuilderService, PageRouteRegistryService, PageTabRegistryService } from '@gauzy/ui-core/core';
import { uiPluginConfig } from './plugin-ui.config';
import { AppModule } from './app.module';
import { AppComponent } from './app.component';
// Set config before Angular bootstrap
setPluginUiConfig(uiPluginConfig);
@NgModule({
imports: [
PluginUiModule.init({
navBuilder: NavMenuBuilderService,
routeRegistry: PageRouteRegistryService,
tabRegistry: PageTabRegistryService,
}),
AppModule,
],
providers: [
{ provide: PLUGIN_UI_CONFIG, useFactory: getPluginUiConfig },
],
bootstrap: [AppComponent],
})
export class AppBootstrapModule {}
Then define your active plugins in a config file:
// plugin-ui.config.ts
import { PluginUiConfig } from '@gauzy/plugin-ui';
import { MyPlugin } from '@gauzy/plugin-my-ui';
export const uiPluginConfig: PluginUiConfig = {
defaultLanguage: 'en',
defaultLocale: 'en-US',
availableLanguages: ['en'],
availableLocales: ['en-US'],
plugins: [MyPlugin],
};
Creating a Plugin
1. Declarative Plugin (recommended)
Use defineDeclarativePlugin when your plugin only needs to register routes, tabs, nav items, or page extensions. No Angular NgModule or service boilerplate required.
// my-plugin.ts
import { defineDeclarativePlugin, PluginRouteInput } from '@gauzy/plugin-ui';
import { MY_ROUTE } from './my-plugin.routes';
export const MyPlugin = defineDeclarativePlugin('my-plugin', {
location: 'my-sections',
routes: [MY_ROUTE as PluginRouteInput],
tabs: [
{
tabsetId: 'dashboard-page',
tabId: 'my-tab',
tabsetType: 'route',
path: '/pages/dashboard/my-tab',
tabTitle: () => 'My Tab',
tabIcon: 'star-outline',
order: 5,
},
],
navMenu: [
{
type: 'section',
sectionId: 'reports',
items: [
{
id: 'my-plugin-report',
link: '/pages/my-report',
data: { translationKey: 'MY_PLUGIN.REPORT' },
},
],
},
],
});
defineDeclarativePlugin auto-generates a bootstrap callback that injects the services registered in PluginUiModule.init(services). No manual injector calls needed.
2. NgModule Plugin
Use definePlugin (or a plain PluginUiDefinition object) when your plugin needs Angular DI-managed class instances, lifecycle hooks, or complex initialization logic.
// my-plugin.module.ts
import { NgModule } from '@angular/core';
import { inject } from '@angular/core';
import {
IOnPluginUiBootstrap,
IOnPluginUiDestroy,
applyDeclarativeRegistrations,
PLUGIN_DEFINITION,
} from '@gauzy/plugin-ui';
import { NavMenuBuilderService, PageRouteRegistryService } from '@gauzy/ui-core/core';
@NgModule({})
export class MyPluginModule implements IOnPluginUiBootstrap, IOnPluginUiDestroy {
private readonly _nav = inject(NavMenuBuilderService);
private readonly _routes = inject(PageRouteRegistryService);
private readonly _def = inject(PLUGIN_DEFINITION);
ngOnPluginBootstrap(): void {
applyDeclarativeRegistrations(this._def, {
navBuilder: this._nav,
pageRouteRegistry: this._routes,
});
console.log('[MyPlugin] bootstrapped');
}
ngOnPluginDestroy(): void {
console.log('[MyPlugin] destroyed');
}
}
// my-plugin.ts
import { definePlugin } from '@gauzy/plugin-ui';
import { MyPluginModule } from './my-plugin.module';
export const MyPlugin = definePlugin('my-plugin', MyPluginModule, {
location: 'my-sections',
});
Plugin options are available inside the module via inject(PLUGIN_OPTIONS):
const options = inject(PLUGIN_OPTIONS) as { featureFlag: boolean };
3. Lazy-Loaded Plugin
For code-splitting heavy plugins, use loadModule to defer the module load until bootstrap:
export const MyPlugin: PluginUiDefinition = {
id: 'my-plugin',
location: 'my-sections',
loadModule: () =>
import('@gauzy/plugin-my-ui').then((m) => m.MyPluginModule),
};
4. Plugin Group (parent + children)
Use definePluginGroup to create a parent plugin that groups child plugins under a shared location. The parent module typically provides a shell route; children register sub-routes beneath it.
// jobs-plugin.ts
import { definePluginGroup } from '@gauzy/plugin-ui';
import { JobsModule } from './jobs.module';
import { JobEmployeePlugin } from './job-employee/job-employee.plugin';
import { JobSearchPlugin } from './job-search/job-search.plugin';
export const JobsPlugin = definePluginGroup('jobs', JobsModule, {
location: 'jobs-sections',
plugins: [JobEmployeePlugin, JobSearchPlugin],
});
Consumer can customize the child list at registration time:
// plugin-ui.config.ts
plugins: [
JobsPlugin.init({
plugins: [JobEmployeePlugin, JobSearchPlugin, JobMatchingPlugin],
}),
]
Plugin Definition Reference
interface PluginUiDefinition {
/** Unique identifier — must be stable across builds. */
id: string;
/** Angular NgModule class (eager). */
module?: Type<any>;
/** Async factory for lazy loading. */
loadModule?: () => Promise<Type<any>>;
/**
* Lightweight callback for declarative-only plugins.
* Auto-generated by defineDeclarativePlugin().
* Called instead of creating a module instance.
*/
bootstrap?: (injector: Injector) => void | Promise<void>;
/** Page-route registry location (e.g. 'page-sections'). */
location?: string;
/** Child plugins (for plugin groups). */
plugins?: PluginUiDefinition[];
/** Per-plugin options — available via inject(PLUGIN_OPTIONS). */
options?: Record<string, unknown>;
/** Declarative routes registered at bootstrap. */
routes?: PluginRouteInput[];
/** Declarative nav menu contributions. */
navMenu?: PluginNavContribution[];
/** Declarative page tabs. */
tabs?: PluginTabInput[];
/** Declarative page extensions. */
extensions?: PageExtensionDefinition[];
/** Feature key for PLUGIN_ACTIVATION_PREDICATE checks. */
featureKey?: unknown;
/** Permission keys for PLUGIN_ACTIVATION_PREDICATE checks. */
permissionKeys?: unknown[];
/** Plugin IDs that must bootstrap before this plugin. */
dependsOn?: string[];
}
Registering Plugins
Add plugins to the plugins array in your PluginUiConfig:
export const uiPluginConfig: PluginUiConfig = {
defaultLanguage: 'en',
defaultLocale: 'en-US',
availableLanguages: ['en', 'fr'],
availableLocales: ['en-US', 'fr-FR'],
plugins: [
IntegrationPlugin,
JobsPlugin.init({ plugins: [JobEmployeePlugin, JobSearchPlugin] }),
ReactUiPlugin,
],
};
Plugins are initialized in declaration order, respecting dependsOn for explicit ordering.
Lifecycle Hooks
NgModule-based plugins can implement these interfaces:
| Interface | Method | Timing |
|---|---|---|
IOnPluginUiBootstrap |
ngOnPluginBootstrap() |
After all modules instantiated |
IOnPluginAfterBootstrap |
ngOnPluginAfterBootstrap() |
After all bootstrap hooks complete |
IOnPluginBeforeDestroy |
ngOnPluginBeforeDestroy() |
Synchronous, before destroy loop |
IOnPluginUiDestroy |
ngOnPluginDestroy() |
On application shutdown |
@NgModule({})
export class MyPluginModule
implements IOnPluginUiBootstrap, IOnPluginAfterBootstrap, IOnPluginUiDestroy
{
ngOnPluginBootstrap(): void { /* register routes, tabs, nav */ }
ngOnPluginAfterBootstrap(): void { /* post-bootstrap setup */ }
ngOnPluginDestroy(): void { /* cleanup */ }
}
Declarative plugins (
defineDeclarativePlugin) do not support lifecycle hooks. If you need hooks, use an NgModule plugin instead.
Declarative Registrations
The applyDeclarativeRegistrations helper applies routes, nav, tabs, and extensions from a PluginUiDefinition object. It's called automatically by defineDeclarativePlugin; call it manually in NgModule lifecycle hooks.
applyDeclarativeRegistrations(pluginDefinition, {
navBuilder: inject(NavMenuBuilderService),
pageRouteRegistry: inject(PageRouteRegistryService),
pageTabRegistry: inject(PageTabRegistryService),
pageExtensionRegistry: inject(PageExtensionRegistryService),
});
Routes
routes: [
{
location: 'page-sections',
path: 'my-feature',
loadChildren: () =>
import('./my-feature/my-feature.module').then((m) => m.MyFeatureModule),
},
],
Nav Menu
Add a new top-level section:
navMenu: [
{
type: 'config',
config: {
id: 'my-plugin',
link: '/pages/my-feature',
data: { translationKey: 'MY_PLUGIN.TITLE' },
},
},
],
Add items to an existing section:
navMenu: [
{
type: 'section',
sectionId: 'reports',
items: [
{
id: 'my-report',
link: '/pages/reports/my-report',
data: { translationKey: 'MY_PLUGIN.REPORT' },
},
],
},
],
Page Tabs
tabs: [
{
tabsetId: 'dashboard-page',
tabId: 'my-widget-tab',
tabsetType: 'route',
path: '/pages/dashboard/my-widget',
tabTitle: (i18n) => i18n.getTranslation('MY_PLUGIN.WIDGET_TAB'),
tabIcon: 'star-outline',
responsive: true,
activeLinkOptions: { exact: false },
order: 10,
permissions: [PermissionsEnum.ADMIN_DASHBOARD_VIEW],
},
],
Extensions
import { defineReactExtension } from '@gauzy/plugin-ui';
import { MyReactWidget } from './components/MyReactWidget';
extensions: [
defineReactExtension({
id: 'my-react-widget',
slotId: PAGE_EXTENSION_SLOTS.DASHBOARD_WIDGETS,
component: MyReactWidget,
props: { title: 'My Widget' },
order: 5,
}),
],
Activation Predicate (Feature Flags / Permissions)
Provide PLUGIN_ACTIVATION_PREDICATE to skip plugins whose feature flag is disabled or whose permissions the user lacks. The predicate runs before each plugin is bootstrapped.
// In your root module providers:
{
provide: PLUGIN_ACTIVATION_PREDICATE,
useFactory: () => {
const store = inject(Store);
return (def: PluginUiDefinition): boolean => {
if (def.featureKey && !store.hasFeatureEnabled(def.featureKey)) {
return false;
}
if (def.permissionKeys?.length && !store.hasAnyPermission(...def.permissionKeys)) {
return false;
}
return true;
};
},
}
Then annotate plugins:
export const MyPlugin = defineDeclarativePlugin('my-plugin', {
featureKey: FeatureEnum.MY_FEATURE,
permissionKeys: [PermissionsEnum.MY_PERMISSION],
routes: [...],
tabs: [...],
});
React Bridge
@gauzy/plugin-ui includes a React-Angular bridge that lets you render React components inside Angular templates, and access Angular services from React.
Rendering React inside Angular
Using ReactHostDirective (eager component)
import { Component } from '@angular/core';
import { ReactHostDirective } from '@gauzy/plugin-ui';
import { MyReactWidget } from './components/MyReactWidget';
@Component({
standalone: true,
imports: [ReactHostDirective],
template: `
<div
[reactHost]="component"
[reactHostProps]="props"
></div>
`,
})
export class MyAngularComponent {
component = MyReactWidget;
props = { title: 'Hello from React!' };
}
Using LazyReactHostDirective (lazy component)
import { LazyReactHostDirective } from '@gauzy/plugin-ui';
@Component({
standalone: true,
imports: [LazyReactHostDirective],
template: `
<div
[lazyReactHost]="componentLoader"
[lazyReactHostProps]="props"
></div>
`,
})
export class MyAngularComponent {
componentLoader = () =>
import('./components/MyReactWidget').then((m) => m.MyReactWidget);
props = { title: 'Lazy Loaded!' };
}
Accessing Angular Services from React
Use the useInjector hook to access any Angular service from within a React component. The Angular Injector is provided automatically via the NgContextProvider.
// MyReactWidget.tsx
import React from 'react';
import { useInjector } from '@gauzy/ui-react';
import { TimesheetStatisticsService } from '@gauzy/ui-core/core';
export function MyReactWidget() {
const injector = useInjector();
const stats = injector.get(TimesheetStatisticsService);
const [data, setData] = React.useState<unknown | null>(null);
React.useEffect(() => {
stats.getStatistics().subscribe((result) => setData(result));
}, []);
return <div>{data ? JSON.stringify(data) : 'Loading...'}</div>;
}
Plugin Events
Use usePluginEvents to subscribe to or emit events across plugin boundaries via the PluginEventBusService.
import { usePluginEvents } from '@gauzy/plugin-ui';
export function MyReactWidget() {
const { emit, subscribe } = usePluginEvents();
React.useEffect(() => {
const sub = subscribe('timer:started', (payload) => {
console.log('Timer started', payload);
});
return () => sub.unsubscribe();
}, []);
return (
<button onClick={() => emit('widget:clicked', { id: 'my-widget' })}>
Click Me
</button>
);
}
Extension Slots
Page extension slots let plugins inject components into pre-defined areas of the host app without modifying its templates.
Using a slot in a host template
import { PageExtensionSlotComponent } from '@gauzy/plugin-ui';
@Component({
standalone: true,
imports: [PageExtensionSlotComponent],
template: `
<gz-page-extension-slot slotId="dashboard-widgets" />
`,
})
export class DashboardComponent {}
Registering a React component as an extension
import { defineReactExtension, PAGE_EXTENSION_SLOTS } from '@gauzy/plugin-ui';
import { MyWidget } from './MyWidget';
// In your plugin definition:
extensions: [
defineReactExtension({
id: 'my-dashboard-widget',
slotId: PAGE_EXTENSION_SLOTS.DASHBOARD_WIDGETS,
component: MyWidget,
props: { mode: 'compact' },
order: 1,
}),
],
Building
# Development build
yarn nx build plugin-ui
# Production build
yarn nx build plugin-ui --configuration=production
# Run tests
yarn nx test plugin-ui