@open-rlb/ng-app 3.1.110 → 3.1.112
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/open-rlb-ng-app.mjs +1 -0
- package/fesm2022/open-rlb-ng-app.mjs.map +1 -1
- package/package.json +10 -2
- package/schematics/collection.json +10 -0
- package/schematics/ng-add/claude-skills/rlb-app-apps/SKILL.md +110 -0
- package/schematics/ng-add/claude-skills/rlb-app-auth-acl/SKILL.md +82 -0
- package/schematics/ng-add/claude-skills/rlb-app-config/SKILL.md +67 -0
- package/schematics/ng-add/claude-skills/rlb-app-shell/SKILL.md +69 -0
- package/schematics/ng-add/claude-skills/rlb-app-store/SKILL.md +49 -0
- package/schematics/ng-add/files/app/app-init-acl.provider.ts.template +54 -0
- package/schematics/ng-add/files/app/app.component.ts.template +44 -0
- package/schematics/ng-add/files/app/app.config.ts.template +17 -0
- package/schematics/ng-add/files/app/app.describer.ts.template +30 -0
- package/schematics/ng-add/files/app/app.routes.ts.template +21 -0
- package/schematics/ng-add/files/app/home/home.component.html.template +4 -0
- package/schematics/ng-add/files/app/home/home.component.ts.template +13 -0
- package/schematics/ng-add/files/assets/i18n/en.json +106 -0
- package/schematics/ng-add/files/assets/i18n/it.json +101 -0
- package/schematics/ng-add/files/environments/environment.ts.template +86 -0
- package/schematics/ng-add/files/main.ts.template +5 -0
- package/schematics/ng-add/index.js +157 -0
- package/schematics/ng-add/schema.js +2 -0
- package/schematics/ng-add/schema.json +26 -0
- package/schematics/package.json +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-rlb/ng-app",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.112",
|
|
4
4
|
"peerDependencies": {
|
|
5
5
|
"@angular/common": ">=21.0.0 <22.0.0",
|
|
6
6
|
"@angular/core": ">=21.0.0 <22.0.0",
|
|
@@ -21,6 +21,10 @@
|
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"tslib": "^2.8.1"
|
|
23
23
|
},
|
|
24
|
+
"schematics": "./schematics/collection.json",
|
|
25
|
+
"ng-add": {
|
|
26
|
+
"save": "dependencies"
|
|
27
|
+
},
|
|
24
28
|
"sideEffects": false,
|
|
25
29
|
"module": "fesm2022/open-rlb-ng-app.mjs",
|
|
26
30
|
"typings": "types/open-rlb-ng-app.d.ts",
|
|
@@ -33,5 +37,9 @@
|
|
|
33
37
|
"default": "./fesm2022/open-rlb-ng-app.mjs"
|
|
34
38
|
}
|
|
35
39
|
},
|
|
36
|
-
"type": "module"
|
|
40
|
+
"type": "module",
|
|
41
|
+
"files": [
|
|
42
|
+
"**/*",
|
|
43
|
+
"schematics/package.json"
|
|
44
|
+
]
|
|
37
45
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../../../node_modules/@angular-devkit/schematics/collection-schema.json",
|
|
3
|
+
"schematics": {
|
|
4
|
+
"ng-add": {
|
|
5
|
+
"description": "Add @open-rlb/ng-app to an Angular project: install peer dependencies, register Bootstrap styles, and scaffold a runnable application shell (environment, providers, app shell, routes, i18n).",
|
|
6
|
+
"factory": "./ng-add/index#ngAdd",
|
|
7
|
+
"schema": "./ng-add/schema.json"
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rlb-app-apps
|
|
3
|
+
description: The @open-rlb/ng-app multi-app orchestration — the AppInfo/AppDescriber model, AppsService (the runtime engine), domain + ACL filtering of apps, current-app selection from the route, and root/deep-link redirect rules. Use when registering apps, building an app hub/selector, controlling which app is "current", or debugging why an app isn't shown or selected.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# @open-rlb/ng-app — Apps & Orchestration
|
|
7
|
+
|
|
8
|
+
`@open-rlb/ng-app` is a **multi-app shell**: a single host hosts one or more logical "apps", each
|
|
9
|
+
described by an `AppInfo`. **`AppsService`** is the runtime engine that filters those apps by
|
|
10
|
+
domain and ACL, selects the current app from the active route, and applies redirect rules. It is
|
|
11
|
+
exported from the public API and **eagerly bootstrapped** by `provideRlbConfig`
|
|
12
|
+
(`provideAppInitializer(() => inject(AppsService))`), so it runs as soon as the app starts.
|
|
13
|
+
|
|
14
|
+
## The app model — `AppInfo`
|
|
15
|
+
|
|
16
|
+
An app is registered via `provideApp(appDescriber)` (`AppDescriber.info` is an `AppInfo`). Fields
|
|
17
|
+
that drive runtime behavior:
|
|
18
|
+
|
|
19
|
+
| Field | Drives |
|
|
20
|
+
|---|---|
|
|
21
|
+
| `type` | App kind/category (e.g. `'app'`); used in `finalizeApp`/route matching. |
|
|
22
|
+
| `enabled` | Whether the app participates. |
|
|
23
|
+
| `core` / `settings` | `AppDetails` — `{ title, description, url, icon, auth }`. `core.url` is the app's landing route and the redirect target. |
|
|
24
|
+
| `viewMode` | `'app'` \| `'settings'` — how the shell frames the app. |
|
|
25
|
+
| `routes` | Route path strings owned by the app; matched against the active URL to select it. |
|
|
26
|
+
| `domains` | If set, the app only appears/activates on those `window.location.hostname`s (domain gating). |
|
|
27
|
+
| `actions` | ACL action names; the app is hidden unless the user holds a matching resource action (ACL gating). |
|
|
28
|
+
| `autoRedirectOnRootEnabled` | At root with multiple apps, redirect to this app's `core.url`. |
|
|
29
|
+
| `data` | Arbitrary payload; carries the ACL `businessIdKey` / `resourceIdKey` values used for permission checks. |
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
export const appDescriber: AppDescriber = {
|
|
33
|
+
info: {
|
|
34
|
+
type: 'app',
|
|
35
|
+
enabled: true,
|
|
36
|
+
actions: ['sysadmin'],
|
|
37
|
+
core: { title: 'My App', description: '', url: '/home', icon: 'bi bi-house', auth: true },
|
|
38
|
+
},
|
|
39
|
+
routes, // Angular Routes; flattened paths are stored on RLB_APPS
|
|
40
|
+
};
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Registration → runtime lifecycle
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
provideApp(appDescriber) register the AppInfo into RLB_APPS (multi)
|
|
47
|
+
→ RLB_INIT_PROVIDER.finalizeApps ACL hook dispatches AppContextActions.finalizeApp → apps gain an `id`
|
|
48
|
+
→ AppsService.apps (computed) filters finalized apps by domain + ACL resources/actions
|
|
49
|
+
→ router listener (NavigationEnd) matches the route → selects currentApp + applies redirects
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
This is the "AppsService orchestration" step of the auth pipeline
|
|
53
|
+
(`checkAuthMultiple → resourcesByUser → finalizeApp → AppsService orchestration`). See
|
|
54
|
+
[[rlb-app-auth-acl]] for the ACL hook and [[rlb-app-config]] for `provideApp`/`RLB_APPS`.
|
|
55
|
+
|
|
56
|
+
## Reading apps from a component
|
|
57
|
+
|
|
58
|
+
Inject `AppsService` and read its signals (zoneless — use signals, never zone.js):
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
private apps = inject(AppsService);
|
|
62
|
+
|
|
63
|
+
readonly visibleApps = this.apps.apps; // signal: domain- + ACL-filtered AppInfo[]
|
|
64
|
+
readonly current = this.apps.currentApp; // signal: AppInfo | null
|
|
65
|
+
readonly currentId = this.apps.currentAppId; // signal: string | undefined
|
|
66
|
+
|
|
67
|
+
canEdit = this.apps.checkPermissionInCurrentApp('catalog-editor'); // ACL in current app
|
|
68
|
+
this.apps.selectApp(app, 'app', '/home'); // manually select (the app hub/selector uses this)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The built-in app/settings selector pages (`pages/apps/*`, `pages/settings/*`) render
|
|
72
|
+
`apps()` and call `selectApp(...)`.
|
|
73
|
+
|
|
74
|
+
## Current-app selection & redirect rules
|
|
75
|
+
|
|
76
|
+
On each `NavigationEnd`, `AppsService` resolves the active route and applies (in order):
|
|
77
|
+
|
|
78
|
+
1. **Single app at root** → auto-redirect to that app's `core.url`.
|
|
79
|
+
2. **Multiple apps at root** → show the app hub (`currentApp` is `null`), unless an app has
|
|
80
|
+
`autoRedirectOnRootEnabled` (then redirect to it).
|
|
81
|
+
3. **Deep link** (non-root) → select the app whose `routes`/`core.url` matches the path.
|
|
82
|
+
|
|
83
|
+
Selection waits until apps are **finalized** (every app has an `id`); until then `currentApp`
|
|
84
|
+
stays unresolved.
|
|
85
|
+
|
|
86
|
+
## ACL on initial deep-link — `findAppForPath`
|
|
87
|
+
|
|
88
|
+
Because `currentApp` isn't selected until `NavigationEnd` fires, `permissionGuard` handles the
|
|
89
|
+
first navigation specially: if no `currentApp` yet, it calls
|
|
90
|
+
`appsService.findAppForPath(path)` to locate the owning app from the URL, then
|
|
91
|
+
`checkPermissionForApp(app, action)`. After that, it uses `checkPermissionInCurrentApp(action)`.
|
|
92
|
+
The `*roles` directive also reads `checkPermissionInCurrentApp` (re-runs reactively). See
|
|
93
|
+
[[rlb-app-auth-acl]].
|
|
94
|
+
|
|
95
|
+
## Multi-provider auth by domain
|
|
96
|
+
|
|
97
|
+
`AppsService.initAuthProviders()` picks the OIDC provider at startup: if one provider, use it; if
|
|
98
|
+
several, match by `provider.domains` against the current hostname. Multiple matches (or none) log
|
|
99
|
+
a warning and leave the provider unset — give each provider a distinct `domains` entry.
|
|
100
|
+
|
|
101
|
+
## Gotchas
|
|
102
|
+
|
|
103
|
+
- **Apps must be finalized** (have an `id`) before selection resolves — driven by the ACL init
|
|
104
|
+
hook; an empty `apps()` usually means ACL/finalize hasn't run yet.
|
|
105
|
+
- **`currentApp()` is `null` at root** in multi-app (hub) mode — that's expected, not an error.
|
|
106
|
+
- **Domain gating uses `window.location.hostname`** — `localhost` won't match production
|
|
107
|
+
`domains`; include `localhost` (or omit `domains`) for local dev.
|
|
108
|
+
- Don't re-run `selectApp` redundantly — it no-ops when the id + viewMode are unchanged.
|
|
109
|
+
|
|
110
|
+
Related: [[rlb-app-config]], [[rlb-app-auth-acl]], [[rlb-app-shell]], [[rlb-app-store]].
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rlb-app-auth-acl
|
|
3
|
+
description: Authentication (OIDC) and authorization (ACL) in @open-rlb/ng-app — OAuth provider config, the oauthGuard and permissionGuard route guards, the *roles structural directive, and the RLB_INIT_PROVIDER ACL startup hook. Use when securing routes/UI or configuring login.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# @open-rlb/ng-app — Auth & ACL
|
|
7
|
+
|
|
8
|
+
Authentication is OIDC via `angular-auth-oidc-client`; authorization is action-based ACL resolved from the backend on startup.
|
|
9
|
+
|
|
10
|
+
## Configuring auth
|
|
11
|
+
|
|
12
|
+
In `environment.ts` under `auth`:
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
auth: {
|
|
16
|
+
protocol: 'oauth',
|
|
17
|
+
storage: 'localStorage', // 'cookies' | 'localStorage' | 'sessionStorage'
|
|
18
|
+
interceptor: 'oauth-code-ep', // attach tokens to endpoint calls
|
|
19
|
+
allowedUrls: ['https://api.example.com'],
|
|
20
|
+
enableCompanyInterceptor: true,
|
|
21
|
+
providers: [{
|
|
22
|
+
configId: 'default',
|
|
23
|
+
authority: 'https://login.example.com/realms/your-realm',
|
|
24
|
+
clientId: 'your-client-id',
|
|
25
|
+
redirectUrl: 'http://localhost:4200',
|
|
26
|
+
postLogoutRedirectUri: 'http://localhost:4200',
|
|
27
|
+
scope: 'openid profile offline_access',
|
|
28
|
+
acl: { endpointKey: 'http-gateway', path: 'admin/acl/resources' }, // where to fetch ACL
|
|
29
|
+
}],
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The startup pipeline is `checkAuthMultiple → resourcesByUser → finalizeApp → AppsService orchestration` (wired by `provideRlbConfig`). The final step — filtering apps by domain/ACL and selecting the current app from the route — is handled by `AppsService`; see [[rlb-app-apps]].
|
|
34
|
+
|
|
35
|
+
## Guarding routes
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
import { oauthGuard, permissionGuard } from '@open-rlb/ng-app';
|
|
39
|
+
|
|
40
|
+
{ path: 'account', component: AccountComponent, canActivate: [oauthGuard] },
|
|
41
|
+
{ path: 'admin', component: AdminComponent, canActivate: [permissionGuard], data: { action: 'sysadmin' } },
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
- `oauthGuard` — requires an authenticated session.
|
|
45
|
+
- `permissionGuard` — requires the ACL action named in `route.data.action`.
|
|
46
|
+
|
|
47
|
+
## Guarding UI
|
|
48
|
+
|
|
49
|
+
The `*roles` structural directive shows content only if the user holds the action:
|
|
50
|
+
|
|
51
|
+
```html
|
|
52
|
+
<button *roles="'sysadmin'">Admin only</button>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
(`RlbAppModule` provides the directive.)
|
|
56
|
+
|
|
57
|
+
## ACL startup hook — RLB_INIT_PROVIDER
|
|
58
|
+
|
|
59
|
+
Implement `RlbInitProvider` to map the user's `UserResource[]` into app instances:
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
@Injectable()
|
|
63
|
+
export class AppInitAclProvider implements RlbInitProvider {
|
|
64
|
+
async finalizeApps(resources: UserResource[], store: Store<BaseState>, acl: AclConfiguration) {
|
|
65
|
+
resources.forEach(company => company.resources.forEach(res => {
|
|
66
|
+
store.dispatch(AppContextActions.finalizeApp({
|
|
67
|
+
appType: 'app',
|
|
68
|
+
appId: `app-${res.resourceId}`,
|
|
69
|
+
data: {
|
|
70
|
+
title: res.friendlyName,
|
|
71
|
+
[acl.businessIdKey]: company.companyId,
|
|
72
|
+
[acl.resourceIdKey]: res.resourceId,
|
|
73
|
+
},
|
|
74
|
+
}));
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Register it: `{ provide: RLB_INIT_PROVIDER, useClass: AppInitAclProvider }`. The action names in `appDescriber.info.actions` must match the ACL actions your backend returns.
|
|
81
|
+
|
|
82
|
+
Related: [[rlb-app-apps]], [[rlb-app-config]], [[rlb-app-store]], [[rlb-app-shell]].
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rlb-app-config
|
|
3
|
+
description: Configuration and bootstrap of an app built on @open-rlb/ng-app — the ProjectConfiguration/environment shape, provideRlbConfig(), provideApp(), the AppDescriber, and the RLB_INIT_PROVIDER startup hook. Use when wiring up app.config.ts, editing environment.ts, or changing app metadata/routes.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# @open-rlb/ng-app — Configuration & Bootstrap
|
|
7
|
+
|
|
8
|
+
You are an expert in bootstrapping an Angular application on the **@open-rlb/ng-app** library. A host app is intentionally thin: it supplies a `ProjectConfiguration` (the environment), an `AppDescriber`, and an optional ACL init provider, then the library wires the store, router, auth, HTTP, i18n and service worker.
|
|
9
|
+
|
|
10
|
+
## app.config.ts — the three building blocks
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
export const appConfig: ApplicationConfig = {
|
|
14
|
+
providers: [
|
|
15
|
+
provideRlbConfig(environment), // store + router + auth + i18n + http + SW + registries
|
|
16
|
+
provideApp(appDescriber), // app identity, ACL actions, routes, custom providers
|
|
17
|
+
{ provide: RLB_INIT_PROVIDER, useClass: AppInitAclProvider }, // optional startup ACL mapping
|
|
18
|
+
provideZonelessChangeDetection(),
|
|
19
|
+
],
|
|
20
|
+
};
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
- `provideRlbConfig(environment)` is the only place the library config is read. It internally calls `provideRlbBootstrap()` (so do NOT call it again), registers the four NgRx feature slices, the default routes derived from `environment.pages`, OIDC auth (`provideRlbCodeBrowserOAuth(env.auth)`), i18n (`provideRlbI18n(env.i18n)`), `provideHttpClient(withInterceptorsFromDi())`, the service worker, and the built-in modal/toast registries.
|
|
24
|
+
- `provideApp(appDescriber)` registers `RLB_APPS` (multi), adds `appDescriber.routes` via `provideRouter`, and spreads any `appDescriber.providers`. At runtime those registered apps are filtered and selected by `AppsService` — see [[rlb-app-apps]].
|
|
25
|
+
|
|
26
|
+
## environment.ts — ProjectConfiguration
|
|
27
|
+
|
|
28
|
+
`ProjectConfiguration = IConfiguration & { production: boolean }`. Sections:
|
|
29
|
+
|
|
30
|
+
| Key | Purpose |
|
|
31
|
+
|---|---|
|
|
32
|
+
| `environment` | app metadata: `appTitle`, `appLogo`, `baseUrl`, `errorDialogName`, `logLevel`, `pwaUpdateEnabled`, `navbarDisabled` |
|
|
33
|
+
| `auth` | OIDC: `protocol: 'oauth'`, `storage`, `interceptor`, `allowedUrls`, `providers[]` (per-provider `authority`/`clientId`/`redirectUrl`/`acl`) |
|
|
34
|
+
| `i18n` | `availableLangs`, `defaultLanguage`, `useLanguageBrowser`, `storeSelectedLanguage`, `cookieStoreName` |
|
|
35
|
+
| `pages` | named route paths for standard pages (notFound, forbidden, support, …) |
|
|
36
|
+
| `endpoints` | named HTTP/WS backends (`{ baseUrl, healthPath, auth, wss }`) referenced by key (e.g. `http-gateway`) |
|
|
37
|
+
| `acl` | `businessIdKey`, `resourceIdKey`, `interceptorMapping` |
|
|
38
|
+
|
|
39
|
+
Inject any slice with the tokens: `RLB_CFG`, `RLB_CFG_ENV`, `RLB_CFG_I18N`, `RLB_CFG_PAGES`, `RLB_CFG_ACL`, `RLB_CFG_CMS`.
|
|
40
|
+
|
|
41
|
+
## AppDescriber
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
export const appDescriber: AppDescriber = {
|
|
45
|
+
info: {
|
|
46
|
+
type: 'app',
|
|
47
|
+
enabled: true,
|
|
48
|
+
actions: ['sysadmin', ...], // ACL action names this app understands
|
|
49
|
+
core: { title, description, url, icon, auth: true },
|
|
50
|
+
settings: { title, description, url, icon, auth: true },
|
|
51
|
+
},
|
|
52
|
+
routes, // Angular Routes; flattened paths are registered with RLB_APPS
|
|
53
|
+
providers: [ /* optional: e.g. RLB_APP_NAVCOMP overrides */ ],
|
|
54
|
+
};
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## RLB_INIT_PROVIDER
|
|
58
|
+
|
|
59
|
+
A class implementing `RlbInitProvider.finalizeApps(resources, store, acl)`. It runs after authentication, turning the user's ACL `UserResource[]` into app instances via `AppContextActions.finalizeApp(...)`. Optional — omit the provider if you don't drive apps from ACL. See [[rlb-app-auth-acl]].
|
|
60
|
+
|
|
61
|
+
## Common gotchas
|
|
62
|
+
|
|
63
|
+
- The app is **zoneless** — never rely on zone.js; use signals.
|
|
64
|
+
- `appLogo`/i18n files live under `src/assets`; ensure `assets` includes `src/assets` in `angular.json`.
|
|
65
|
+
- The service worker is `enabled: !isDevMode()`, so it's off under `ng serve`. For production builds, configure `ngsw-config.json` (e.g. `ng add @angular/pwa`).
|
|
66
|
+
|
|
67
|
+
Related: [[rlb-app-apps]], [[rlb-app-shell]], [[rlb-app-store]], [[rlb-app-auth-acl]].
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rlb-app-shell
|
|
3
|
+
description: The @open-rlb/ng-app application shell — <rlb-app-container>, navbar/sidebar configuration via NgRx actions, language selection, and custom navbar item components (RLB_APP_NAVCOMP). Use when building or customizing the app layout/chrome.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# @open-rlb/ng-app — Application Shell
|
|
7
|
+
|
|
8
|
+
The shell renders the full app chrome (navbar, sidebar, modal/toast containers, routed content). The host `AppComponent` is thin: it mounts `<rlb-app-container>` and dispatches store actions to configure visibility and items.
|
|
9
|
+
|
|
10
|
+
## Mounting the shell
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
@Component({
|
|
14
|
+
selector: 'app-root',
|
|
15
|
+
imports: [RlbAppModule],
|
|
16
|
+
template: `
|
|
17
|
+
<rlb-app-container
|
|
18
|
+
modal-container-id="modal-c-1"
|
|
19
|
+
toast-container-ids="toast-c-1"
|
|
20
|
+
/>`,
|
|
21
|
+
})
|
|
22
|
+
export class AppComponent { /* dispatch config in constructor */ }
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`RlbAppModule` aggregates the library's standalone components/directives/pipes; import it wherever you use shell elements (`rlb-app-container`, `rlb-navbar-item`, the `*roles` directive, etc.).
|
|
26
|
+
|
|
27
|
+
## Configuring navbar & sidebar (NgRx actions)
|
|
28
|
+
|
|
29
|
+
Dispatch in `AppComponent`'s constructor:
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
this.store.dispatch(NavbarActions.setLoginVisible({ visible: true }));
|
|
33
|
+
this.store.dispatch(NavbarActions.setSettingsVisible({ visible: true }));
|
|
34
|
+
this.store.dispatch(NavbarActions.setAppsVisible({ visible: true }));
|
|
35
|
+
|
|
36
|
+
this.store.dispatch(SidebarActions.setAppsVisible({ visible: true }));
|
|
37
|
+
this.store.dispatch(SidebarActions.setItems({
|
|
38
|
+
items: [
|
|
39
|
+
{ title: 'Menu' }, // section header
|
|
40
|
+
{ label: 'Home', url: '/', icon: 'bi bi-house' },
|
|
41
|
+
{ label: 'Profile', url: '/profile', icon: 'bi bi-person', badgeCounter: 3 },
|
|
42
|
+
{ label: 'Links', icon: 'bi bi-link', items: [ // nested
|
|
43
|
+
{ label: 'GitHub', icon: 'bi bi-github', externalUrl: 'https://github.com' },
|
|
44
|
+
]},
|
|
45
|
+
],
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
this.store.dispatch(AppContextActions.setSupportedLanguages({ supportedLanguages: ['en', 'it'] }));
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Sidebar item shape: `{ title }` (header) or `{ label, url? | externalUrl?, icon?, badgeCounter?, items? }`.
|
|
52
|
+
|
|
53
|
+
## Custom navbar item components
|
|
54
|
+
|
|
55
|
+
Override navbar slots by providing `RLB_APP_NAVCOMP` (via `appDescriber.providers`):
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
{
|
|
59
|
+
provide: RLB_APP_NAVCOMP,
|
|
60
|
+
useValue: {
|
|
61
|
+
left: [{ component: MyNavItemComponent, name: 'my-item' }],
|
|
62
|
+
right: [{ component: MyNavItemComponent, name: 'my-item' }],
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Icons are Bootstrap Icons (`bi bi-*`), available because the shell registers `bootstrap-icons.css`.
|
|
68
|
+
|
|
69
|
+
Related: [[rlb-app-config]], [[rlb-app-apps]], [[rlb-app-store]], [[rlb-app-auth-acl]].
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rlb-app-store
|
|
3
|
+
description: The @open-rlb/ng-app NgRx store — the auth, appContext, navbars, and sidebars feature slices, their action groups, and reading state with signals. Use when dispatching actions or selecting app/auth/navbar/sidebar state.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# @open-rlb/ng-app — Store (NgRx)
|
|
7
|
+
|
|
8
|
+
The library owns the store. `provideRlbConfig()` registers four feature slices; the host app dispatches actions and selects state. Prefer **signals** (`store.selectSignal`, `toSignal`) over the `async` pipe — the app is zoneless.
|
|
9
|
+
|
|
10
|
+
## Feature slices
|
|
11
|
+
|
|
12
|
+
| Feature key | Purpose | Actions |
|
|
13
|
+
|---|---|---|
|
|
14
|
+
| `auth` | login/logout, user info, token | `AuthActions` |
|
|
15
|
+
| `app` (`appContextFeatureKey`) | current app, apps list, theme, language | `AppContextActions` |
|
|
16
|
+
| `navbars` | navbar items, layout mode, visibility | `NavbarActions` |
|
|
17
|
+
| `sidebars` | sidebar items (nested), visibility | `SidebarActions` |
|
|
18
|
+
|
|
19
|
+
`BaseState` is the combined root state (`AclState & AuthState & SidebarState & NavbarState & AppState`). Type your store as `Store<BaseState>`.
|
|
20
|
+
|
|
21
|
+
## Reading state
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
private store = inject(Store<BaseState>);
|
|
25
|
+
|
|
26
|
+
// signal (preferred)
|
|
27
|
+
currentApp = this.store.selectSignal(s => s[appContextFeatureKey].currentApp);
|
|
28
|
+
|
|
29
|
+
// or observable
|
|
30
|
+
this.store.select(s => s[appContextFeatureKey].apps).subscribe(/* … */);
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Common actions
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
AppContextActions.setSupportedLanguages({ supportedLanguages: ['en', 'it'] });
|
|
37
|
+
AppContextActions.finalizeApp({ appType, appId, data }); // register an app instance (see ACL)
|
|
38
|
+
|
|
39
|
+
NavbarActions.setLoginVisible({ visible });
|
|
40
|
+
NavbarActions.setSettingsVisible({ visible });
|
|
41
|
+
NavbarActions.setAppsVisible({ visible });
|
|
42
|
+
|
|
43
|
+
SidebarActions.setItems({ items });
|
|
44
|
+
SidebarActions.setAppsVisible({ visible });
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`*Internal` action groups (e.g. `AppContextActionsInternal`) are driven by the library's effects — don't dispatch them from app code.
|
|
48
|
+
|
|
49
|
+
Related: [[rlb-app-shell]], [[rlb-app-apps]], [[rlb-app-auth-acl]], [[rlb-app-config]].
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Injectable } from '@angular/core';
|
|
2
|
+
import { Store } from '@ngrx/store';
|
|
3
|
+
import {
|
|
4
|
+
AclConfiguration,
|
|
5
|
+
AppContextActions,
|
|
6
|
+
appContextFeatureKey,
|
|
7
|
+
BaseState,
|
|
8
|
+
RlbInitProvider,
|
|
9
|
+
UserResource,
|
|
10
|
+
} from '@open-rlb/ng-app';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Runs once on startup after authentication: turns the user's ACL resources into
|
|
14
|
+
* app instances via `finalizeApp`. This is an example — adapt `appType`/`appId`
|
|
15
|
+
* and the data mapping to your domain.
|
|
16
|
+
*/
|
|
17
|
+
@Injectable()
|
|
18
|
+
export class AppInitAclProvider implements RlbInitProvider {
|
|
19
|
+
async finalizeApps(
|
|
20
|
+
resources: UserResource[],
|
|
21
|
+
store: Store<BaseState>,
|
|
22
|
+
acl: AclConfiguration,
|
|
23
|
+
): Promise<void> {
|
|
24
|
+
if (!resources || resources.length === 0) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const currentApps = store.selectSignal(state => state[appContextFeatureKey].apps)();
|
|
29
|
+
|
|
30
|
+
resources.forEach(company => {
|
|
31
|
+
company.resources.forEach(res => {
|
|
32
|
+
const appId = `app-${res.resourceId}`;
|
|
33
|
+
if (currentApps.findIndex(app => app.id === appId) === -1) {
|
|
34
|
+
const appData = {
|
|
35
|
+
title: res.friendlyName,
|
|
36
|
+
appName: res.friendlyName,
|
|
37
|
+
[acl.businessIdKey]: company.companyId,
|
|
38
|
+
[acl.resourceIdKey]: res.resourceId,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
store.dispatch(
|
|
42
|
+
AppContextActions.finalizeApp({
|
|
43
|
+
appType: 'app',
|
|
44
|
+
appId,
|
|
45
|
+
data: appData,
|
|
46
|
+
}),
|
|
47
|
+
);
|
|
48
|
+
} else {
|
|
49
|
+
console.warn(`[AppInitAclProvider]: appId ${appId} exists already, skip finalize`);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Component } from '@angular/core';
|
|
2
|
+
import { Store } from '@ngrx/store';
|
|
3
|
+
import {
|
|
4
|
+
AppContextActions,
|
|
5
|
+
BaseState,
|
|
6
|
+
NavbarActions,
|
|
7
|
+
RlbAppModule,
|
|
8
|
+
SidebarActions,
|
|
9
|
+
} from '@open-rlb/ng-app';
|
|
10
|
+
|
|
11
|
+
@Component({
|
|
12
|
+
selector: 'app-root',
|
|
13
|
+
imports: [RlbAppModule],
|
|
14
|
+
template: `
|
|
15
|
+
<rlb-app-container
|
|
16
|
+
modal-container-id="modal-c-1"
|
|
17
|
+
toast-container-ids="toast-c-1"
|
|
18
|
+
/>
|
|
19
|
+
`,
|
|
20
|
+
})
|
|
21
|
+
export class AppComponent {
|
|
22
|
+
constructor(public store: Store<BaseState>) {
|
|
23
|
+
this.store.dispatch(
|
|
24
|
+
AppContextActions.setSupportedLanguages({
|
|
25
|
+
supportedLanguages: ['en', 'it'],
|
|
26
|
+
}),
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
this.store.dispatch(NavbarActions.setLoginVisible({ visible: true }));
|
|
30
|
+
this.store.dispatch(NavbarActions.setSettingsVisible({ visible: true }));
|
|
31
|
+
this.store.dispatch(NavbarActions.setAppsVisible({ visible: true }));
|
|
32
|
+
this.store.dispatch(SidebarActions.setLoginVisible({ visible: false }));
|
|
33
|
+
this.store.dispatch(SidebarActions.setSettingsVisible({ visible: false }));
|
|
34
|
+
this.store.dispatch(SidebarActions.setAppsVisible({ visible: true }));
|
|
35
|
+
this.store.dispatch(
|
|
36
|
+
SidebarActions.setItems({
|
|
37
|
+
items: [
|
|
38
|
+
{ title: 'Menu' },
|
|
39
|
+
{ label: 'Home', url: '/', icon: 'bi bi-house' },
|
|
40
|
+
],
|
|
41
|
+
}),
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ApplicationConfig, provideZonelessChangeDetection } from '@angular/core';
|
|
2
|
+
import { provideApp, provideRlbConfig, RLB_INIT_PROVIDER } from '@open-rlb/ng-app';
|
|
3
|
+
import { environment } from '../environments/environment';
|
|
4
|
+
import { appDescriber } from './app.describer';
|
|
5
|
+
import { AppInitAclProvider } from './app-init-acl.provider';
|
|
6
|
+
|
|
7
|
+
export const appConfig: ApplicationConfig = {
|
|
8
|
+
providers: [
|
|
9
|
+
provideRlbConfig(environment),
|
|
10
|
+
provideApp(appDescriber),
|
|
11
|
+
{
|
|
12
|
+
provide: RLB_INIT_PROVIDER,
|
|
13
|
+
useClass: AppInitAclProvider,
|
|
14
|
+
},
|
|
15
|
+
provideZonelessChangeDetection(),
|
|
16
|
+
],
|
|
17
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { AppDescriber } from '@open-rlb/ng-app';
|
|
2
|
+
import { routes } from './app.routes';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Describes this app's identity and capabilities to @open-rlb/ng-app.
|
|
6
|
+
* `actions` are the ACL action names this app understands (used by `permissionGuard`
|
|
7
|
+
* and the `*roles` directive). Adjust `info` and `actions` to match your app.
|
|
8
|
+
*/
|
|
9
|
+
export const appDescriber: AppDescriber = {
|
|
10
|
+
info: {
|
|
11
|
+
type: 'app',
|
|
12
|
+
enabled: true,
|
|
13
|
+
actions: ['sysadmin'],
|
|
14
|
+
core: {
|
|
15
|
+
title: 'Home',
|
|
16
|
+
description: 'Application home',
|
|
17
|
+
url: 'home',
|
|
18
|
+
icon: 'bi bi-house',
|
|
19
|
+
auth: true,
|
|
20
|
+
},
|
|
21
|
+
settings: {
|
|
22
|
+
title: 'Settings',
|
|
23
|
+
description: 'Application settings',
|
|
24
|
+
url: 'settings',
|
|
25
|
+
icon: 'bi bi-gear',
|
|
26
|
+
auth: true,
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
routes,
|
|
30
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Routes } from '@angular/router';
|
|
2
|
+
import { HomeComponent } from './home/home.component';
|
|
3
|
+
|
|
4
|
+
export const routes: Routes = [
|
|
5
|
+
{
|
|
6
|
+
path: '',
|
|
7
|
+
data: { template: 'app' },
|
|
8
|
+
component: HomeComponent,
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
path: 'home',
|
|
12
|
+
component: HomeComponent,
|
|
13
|
+
},
|
|
14
|
+
// Example of a route guarded by an ACL action:
|
|
15
|
+
// {
|
|
16
|
+
// path: 'protected',
|
|
17
|
+
// component: ProtectedComponent,
|
|
18
|
+
// canActivate: [permissionGuard],
|
|
19
|
+
// data: { action: 'sysadmin' },
|
|
20
|
+
// },
|
|
21
|
+
];
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { CommonModule } from '@angular/common';
|
|
2
|
+
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
|
3
|
+
import { RlbAppModule } from '@open-rlb/ng-app';
|
|
4
|
+
|
|
5
|
+
@Component({
|
|
6
|
+
selector: 'app-home',
|
|
7
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
8
|
+
imports: [CommonModule, RlbAppModule],
|
|
9
|
+
templateUrl: './home.component.html',
|
|
10
|
+
})
|
|
11
|
+
export class HomeComponent {
|
|
12
|
+
title = 'Home';
|
|
13
|
+
}
|