@open-rlb/ng-app 3.1.111 → 3.1.113
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/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.113",
|
|
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
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
{
|
|
2
|
+
"common": {
|
|
3
|
+
"ok": "Ok",
|
|
4
|
+
"cancel": "Cancel",
|
|
5
|
+
"close": "Close",
|
|
6
|
+
"save": "Save",
|
|
7
|
+
"delete": "Delete",
|
|
8
|
+
"add": "Add",
|
|
9
|
+
"edit": "Edit",
|
|
10
|
+
"name": "Name",
|
|
11
|
+
"description": "Description",
|
|
12
|
+
"search": "Search",
|
|
13
|
+
"searching": "Searching...",
|
|
14
|
+
"noResults": "No results",
|
|
15
|
+
"loading": "Loading...",
|
|
16
|
+
"yes": "Yes",
|
|
17
|
+
"no": "No"
|
|
18
|
+
},
|
|
19
|
+
"core": {
|
|
20
|
+
"apps": {
|
|
21
|
+
"button": "Apps",
|
|
22
|
+
"title": "Riolabs Apps"
|
|
23
|
+
},
|
|
24
|
+
"settings": {
|
|
25
|
+
"title": "Settings",
|
|
26
|
+
"generalTitle": "General settings",
|
|
27
|
+
"generalDescription": "Manage general settings of applications",
|
|
28
|
+
"support": "Support",
|
|
29
|
+
"privacy": "Privacy",
|
|
30
|
+
"cookies": "Cookies",
|
|
31
|
+
"legal": "Legal",
|
|
32
|
+
"status": "Status",
|
|
33
|
+
"logger": "Logger"
|
|
34
|
+
},
|
|
35
|
+
"account": {
|
|
36
|
+
"title": "Account",
|
|
37
|
+
"email": "Email",
|
|
38
|
+
"changePassword": "Change password",
|
|
39
|
+
"firstName": "First name",
|
|
40
|
+
"lastName": "Last name",
|
|
41
|
+
"login": "Login",
|
|
42
|
+
"logout": "Logout",
|
|
43
|
+
"username": "Username",
|
|
44
|
+
"session": {
|
|
45
|
+
"title": "Sessions",
|
|
46
|
+
"current": "Current session",
|
|
47
|
+
"ip": "IP",
|
|
48
|
+
"applications": "Applications",
|
|
49
|
+
"lastAccess": "Last access",
|
|
50
|
+
"started": "Started",
|
|
51
|
+
"expires": "Expires"
|
|
52
|
+
},
|
|
53
|
+
"credentials": {
|
|
54
|
+
"title": "Credentials",
|
|
55
|
+
"password": "Password",
|
|
56
|
+
"otp": "One-time password",
|
|
57
|
+
"name": "Name",
|
|
58
|
+
"created": "Created",
|
|
59
|
+
"actions": "Actions",
|
|
60
|
+
"update": "Update",
|
|
61
|
+
"delete": "Delete",
|
|
62
|
+
"noCredentials": "No credentials found for this section",
|
|
63
|
+
"addOTP": "Configure OTP",
|
|
64
|
+
"deleteTitle": "Delete credential",
|
|
65
|
+
"deleteHeader": "Delete credential confirmation",
|
|
66
|
+
"deleteMessage": "Are you sure you want to delete this credential?"
|
|
67
|
+
},
|
|
68
|
+
"settings": {
|
|
69
|
+
"title": "Account settings",
|
|
70
|
+
"showMore": "Show more"
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
"pwa": {
|
|
74
|
+
"newVersionAvailable": "New version available",
|
|
75
|
+
"updateNow": "Update now",
|
|
76
|
+
"updateMessage": "A new version of the application is available. Do you want to update it now?",
|
|
77
|
+
"later": "Later",
|
|
78
|
+
"update": "Update"
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
"pages": {
|
|
82
|
+
"notFound": {
|
|
83
|
+
"title": "Page not found",
|
|
84
|
+
"content": "The page you are looking for does not exist.",
|
|
85
|
+
"button": "Go to home page"
|
|
86
|
+
},
|
|
87
|
+
"forbidden": {
|
|
88
|
+
"title": "Access forbidden",
|
|
89
|
+
"content": "You do not have permission to access this page.",
|
|
90
|
+
"button": "Go to home page"
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
"support": {
|
|
94
|
+
"title": "Support",
|
|
95
|
+
"legal": "Legal",
|
|
96
|
+
"name": "Name",
|
|
97
|
+
"email": "Email",
|
|
98
|
+
"subject": "Subject",
|
|
99
|
+
"message": "Message",
|
|
100
|
+
"send": "Send",
|
|
101
|
+
"success": "Your message has been sent successfully.",
|
|
102
|
+
"error": "An error occurred while sending your message.",
|
|
103
|
+
"infoPage": "For more information, please visit our legal page"
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
{
|
|
2
|
+
"common": {
|
|
3
|
+
"ok": "Ok",
|
|
4
|
+
"cancel": "Annulla",
|
|
5
|
+
"close": "Chiudi",
|
|
6
|
+
"save": "Salva",
|
|
7
|
+
"delete": "Elimina",
|
|
8
|
+
"add": "Aggiungi",
|
|
9
|
+
"edit": "Modifica",
|
|
10
|
+
"name": "Nome",
|
|
11
|
+
"description": "Descrizione",
|
|
12
|
+
"search": "Cerca",
|
|
13
|
+
"searching": "Ricerca in corso...",
|
|
14
|
+
"noResults": "Nessun risultato",
|
|
15
|
+
"loading": "Caricamento...",
|
|
16
|
+
"yes": "Sì",
|
|
17
|
+
"no": "No"
|
|
18
|
+
},
|
|
19
|
+
"core": {
|
|
20
|
+
"apps": {
|
|
21
|
+
"button": "Applicazioni",
|
|
22
|
+
"title": "App di Riolabs"
|
|
23
|
+
},
|
|
24
|
+
"settings": {
|
|
25
|
+
"title": "Impostazioni",
|
|
26
|
+
"generalTitle": "Impostazioni generali",
|
|
27
|
+
"generalDescription": "Gestisci le impostazioni generali delle applicazioni",
|
|
28
|
+
"support": "Supporto",
|
|
29
|
+
"privacy": "Privacy",
|
|
30
|
+
"cookies": "Cookie",
|
|
31
|
+
"legal": "Legale",
|
|
32
|
+
"status": "Stato",
|
|
33
|
+
"logger": "Logger"
|
|
34
|
+
},
|
|
35
|
+
"account": {
|
|
36
|
+
"title": "Account",
|
|
37
|
+
"email": "Email",
|
|
38
|
+
"changePassword": "Cambia password",
|
|
39
|
+
"firstName": "Nome",
|
|
40
|
+
"lastName": "Cognome",
|
|
41
|
+
"login": "Accesso",
|
|
42
|
+
"logout": "Esci",
|
|
43
|
+
"username": "Nome utente",
|
|
44
|
+
"session": {
|
|
45
|
+
"title": "Sessioni",
|
|
46
|
+
"current": "Sessione corrente",
|
|
47
|
+
"ip": "IP",
|
|
48
|
+
"applications": "Applicazioni",
|
|
49
|
+
"lastAccess": "Ultimo accesso",
|
|
50
|
+
"started": "Iniziata",
|
|
51
|
+
"expires": "Scade il"
|
|
52
|
+
},
|
|
53
|
+
"credentials": {
|
|
54
|
+
"title": "Credenziali",
|
|
55
|
+
"password": "Password",
|
|
56
|
+
"otp": "Password monouso",
|
|
57
|
+
"name": "Nome",
|
|
58
|
+
"created": "Creato",
|
|
59
|
+
"actions": "Azioni",
|
|
60
|
+
"update": "Aggiorna",
|
|
61
|
+
"delete": "Elimina",
|
|
62
|
+
"noCredentials": "Nessuna credenziale trovata per questa sezione",
|
|
63
|
+
"addOTP": "Configura OTP",
|
|
64
|
+
"deleteTitle": "Elimina credenziale",
|
|
65
|
+
"deleteHeader": "Conferma eliminazione credenziale",
|
|
66
|
+
"deleteMessage": "Sei sicuro di voler eliminare questa credenziale?"
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"pwa": {
|
|
70
|
+
"newVersionAvailable": "Nuova versione disponibile",
|
|
71
|
+
"updateNow": "Aggiorna ora",
|
|
72
|
+
"updateMessage": "È disponibile una nuova versione dell'applicazione. Vuoi aggiornarla ora?",
|
|
73
|
+
"later": "Dopo",
|
|
74
|
+
"update": "Aggiorna"
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
"pages": {
|
|
78
|
+
"notFound": {
|
|
79
|
+
"title": "Pagina non trovata",
|
|
80
|
+
"content": "La pagina che stai cercando non esiste.",
|
|
81
|
+
"button": "Vai alla home page"
|
|
82
|
+
},
|
|
83
|
+
"forbidden": {
|
|
84
|
+
"title": "Accesso negato",
|
|
85
|
+
"content": "Non hai i permessi necessari per accedere a questa pagina.",
|
|
86
|
+
"button": "Vai alla home page"
|
|
87
|
+
},
|
|
88
|
+
"support": {
|
|
89
|
+
"title": "Supporto",
|
|
90
|
+
"legal": "Legale",
|
|
91
|
+
"name": "Nome",
|
|
92
|
+
"email": "Email",
|
|
93
|
+
"subject": "Oggetto",
|
|
94
|
+
"message": "Messaggio",
|
|
95
|
+
"send": "Invia",
|
|
96
|
+
"success": "Il tuo messaggio è stato inviato con successo.",
|
|
97
|
+
"error": "Si è verificato un errore durante l'invio del messaggio.",
|
|
98
|
+
"infoPage": "Per maggiori informazioni, visita la nostra pagina legale"
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { ProjectConfiguration } from '@open-rlb/ng-app';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Application configuration consumed by `provideRlbConfig(environment)`.
|
|
5
|
+
*
|
|
6
|
+
* ⚠ Replace the placeholder values below with your real settings:
|
|
7
|
+
* - auth.providers[].authority / clientId / redirectUrl / postLogoutRedirectUri
|
|
8
|
+
* - auth.allowedUrls and endpoints[].baseUrl (your API gateway URLs)
|
|
9
|
+
*/
|
|
10
|
+
export const environment: ProjectConfiguration = {
|
|
11
|
+
production: false,
|
|
12
|
+
environment: {
|
|
13
|
+
baseUrl: 'http://localhost:4200',
|
|
14
|
+
ssr: false,
|
|
15
|
+
phone: false,
|
|
16
|
+
appLogo: '', // set to your logo path, e.g. 'assets/logo.svg' (place the file under src/assets)
|
|
17
|
+
appTitle: 'My App',
|
|
18
|
+
navbarDisabled: false,
|
|
19
|
+
errorDialogName: 'error-modal-component',
|
|
20
|
+
pwaUpdateEnabled: true,
|
|
21
|
+
errorDialogSize: 'md',
|
|
22
|
+
logLevel: 'log',
|
|
23
|
+
},
|
|
24
|
+
i18n: {
|
|
25
|
+
availableLangs: ['en', 'it'],
|
|
26
|
+
defaultLanguage: 'en',
|
|
27
|
+
useLanguageBrowser: true,
|
|
28
|
+
storeSelectedLanguage: true,
|
|
29
|
+
cookieStoreName: 'language',
|
|
30
|
+
},
|
|
31
|
+
auth: {
|
|
32
|
+
protocol: 'oauth',
|
|
33
|
+
storage: 'localStorage',
|
|
34
|
+
interceptor: 'oauth-code-ep',
|
|
35
|
+
allowedUrls: ['https://api.example.com'],
|
|
36
|
+
enableCompanyInterceptor: true,
|
|
37
|
+
providers: [
|
|
38
|
+
{
|
|
39
|
+
configId: 'default',
|
|
40
|
+
authority: 'https://login.example.com/realms/your-realm',
|
|
41
|
+
domains: ['localhost'],
|
|
42
|
+
redirectUrl: 'http://localhost:4200',
|
|
43
|
+
postLogoutRedirectUri: 'http://localhost:4200',
|
|
44
|
+
clientId: 'your-client-id',
|
|
45
|
+
scope: 'openid profile offline_access',
|
|
46
|
+
logLevel: 2,
|
|
47
|
+
acl: {
|
|
48
|
+
endpointKey: 'http-gateway',
|
|
49
|
+
path: 'admin/acl/resources',
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
pages: {
|
|
55
|
+
content: { path: 'content' },
|
|
56
|
+
cookies: { path: 'cookies' },
|
|
57
|
+
notFound: { path: 'not-found' },
|
|
58
|
+
forbidden: { path: 'forbidden' },
|
|
59
|
+
privacy: { path: 'privacy' },
|
|
60
|
+
support: { path: 'support' },
|
|
61
|
+
terms: { path: 'terms' },
|
|
62
|
+
status: { path: 'status' },
|
|
63
|
+
logger: { path: 'logger' },
|
|
64
|
+
},
|
|
65
|
+
endpoints: {
|
|
66
|
+
'http-gateway': {
|
|
67
|
+
baseUrl: 'https://api.example.com',
|
|
68
|
+
auth: true,
|
|
69
|
+
healthPath: 'health',
|
|
70
|
+
wss: false,
|
|
71
|
+
},
|
|
72
|
+
'ws-gateway': {
|
|
73
|
+
baseUrl: 'wss://api.example.com',
|
|
74
|
+
healthPath: 'health',
|
|
75
|
+
auth: true,
|
|
76
|
+
wss: true,
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
acl: {
|
|
80
|
+
businessIdKey: 'businessId',
|
|
81
|
+
resourceIdKey: 'resourceId',
|
|
82
|
+
interceptorMapping: {
|
|
83
|
+
companyId: 'companyId',
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ngAdd = ngAdd;
|
|
4
|
+
const core_1 = require("@angular-devkit/core");
|
|
5
|
+
const schematics_1 = require("@angular-devkit/schematics");
|
|
6
|
+
const utility_1 = require("@schematics/angular/utility");
|
|
7
|
+
/**
|
|
8
|
+
* Dependencies the library needs at the consumer side. `@angular/{core,common,forms,router}`
|
|
9
|
+
* and `rxjs` are intentionally omitted: every Angular app already provides them. Versions mirror
|
|
10
|
+
* the ranges declared in the library's `peerDependencies`.
|
|
11
|
+
*/
|
|
12
|
+
const DEPENDENCIES = [
|
|
13
|
+
{ name: '@open-rlb/ng-bootstrap', version: '^3.0.1', type: utility_1.DependencyType.Default },
|
|
14
|
+
{ name: '@open-rlb/date-tz', version: '^2.1.1', type: utility_1.DependencyType.Default },
|
|
15
|
+
{ name: '@ngrx/store', version: '^21.0.0', type: utility_1.DependencyType.Default },
|
|
16
|
+
{ name: '@ngrx/effects', version: '^21.0.0', type: utility_1.DependencyType.Default },
|
|
17
|
+
{ name: '@ngrx/operators', version: '^21.0.0', type: utility_1.DependencyType.Default },
|
|
18
|
+
{ name: '@ngrx/signals', version: '^21.0.0', type: utility_1.DependencyType.Default },
|
|
19
|
+
{ name: '@ngx-translate/core', version: '^17.0.0', type: utility_1.DependencyType.Default },
|
|
20
|
+
{ name: '@ngx-translate/http-loader', version: '^17.0.0', type: utility_1.DependencyType.Default },
|
|
21
|
+
{ name: 'angular-auth-oidc-client', version: '^19.0.1', type: utility_1.DependencyType.Default },
|
|
22
|
+
{ name: 'ngx-cookie-service', version: '^21.1.0', type: utility_1.DependencyType.Default },
|
|
23
|
+
{ name: 'ngx-cookie-service-ssr', version: '^21.1.0', type: utility_1.DependencyType.Default },
|
|
24
|
+
{ name: '@angular/service-worker', version: '^21.0.0', type: utility_1.DependencyType.Default },
|
|
25
|
+
{ name: 'bootstrap-icons', version: '^1.13.1', type: utility_1.DependencyType.Default },
|
|
26
|
+
{ name: '@types/bootstrap', version: '^5.2.10', type: utility_1.DependencyType.Dev },
|
|
27
|
+
];
|
|
28
|
+
/** Global styles required for the Bootstrap + @open-rlb/ng-bootstrap look & feel. */
|
|
29
|
+
const STYLE_PATHS = [
|
|
30
|
+
'node_modules/bootstrap-icons/font/bootstrap-icons.css',
|
|
31
|
+
'node_modules/@open-rlb/ng-bootstrap/assets/scss/app.scss',
|
|
32
|
+
'node_modules/@open-rlb/ng-bootstrap/assets/scss/icons.scss',
|
|
33
|
+
];
|
|
34
|
+
/** SCSS `@use`/`@import` resolution root needed by the ng-bootstrap stylesheets. */
|
|
35
|
+
const STYLE_INCLUDE_PATH = 'node_modules';
|
|
36
|
+
/** Source folder for the scaffolded runtime assets (i18n JSON, logo), served at `/assets`. */
|
|
37
|
+
const ASSETS_INPUT = 'src/assets';
|
|
38
|
+
function ngAdd(options) {
|
|
39
|
+
return async (tree, _context) => {
|
|
40
|
+
const project = await resolveProject(tree, options.project);
|
|
41
|
+
return (0, schematics_1.chain)([
|
|
42
|
+
// 1. Install dependencies (a single npm install is scheduled automatically).
|
|
43
|
+
...DEPENDENCIES.map(dep => (0, utility_1.addDependency)(dep.name, dep.version, { type: dep.type })),
|
|
44
|
+
// 2. Register Bootstrap + ng-bootstrap global styles and the SCSS include path in angular.json.
|
|
45
|
+
addBootstrapStyles(project),
|
|
46
|
+
// 3. Scaffold the runnable application shell (providers, environment, app component, routes).
|
|
47
|
+
options.skipShell ? noop : scaffoldShell(tree, project),
|
|
48
|
+
// 4. Optionally copy the bundled Claude skills into .claude/skills.
|
|
49
|
+
options.skipSkills ? noop : copyClaudeSkills(),
|
|
50
|
+
// 5. Print next steps.
|
|
51
|
+
logNextSteps(project, options),
|
|
52
|
+
]);
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/** Resolves the target project: the provided name, else the first application, else the first project. */
|
|
56
|
+
async function resolveProject(tree, name) {
|
|
57
|
+
const workspace = await (0, utility_1.readWorkspace)(tree);
|
|
58
|
+
if (name) {
|
|
59
|
+
if (!workspace.projects.has(name)) {
|
|
60
|
+
throw new schematics_1.SchematicsException(`Project "${name}" was not found in the workspace.`);
|
|
61
|
+
}
|
|
62
|
+
return name;
|
|
63
|
+
}
|
|
64
|
+
for (const [projectName, project] of workspace.projects) {
|
|
65
|
+
if (project.extensions['projectType'] === 'application') {
|
|
66
|
+
return projectName;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const first = workspace.projects.keys().next().value;
|
|
70
|
+
if (!first) {
|
|
71
|
+
throw new schematics_1.SchematicsException('No project found in the workspace to add @open-rlb/ng-app to.');
|
|
72
|
+
}
|
|
73
|
+
return first;
|
|
74
|
+
}
|
|
75
|
+
function addBootstrapStyles(project) {
|
|
76
|
+
return (0, utility_1.updateWorkspace)(workspace => {
|
|
77
|
+
const target = workspace.projects.get(project)?.targets.get('build');
|
|
78
|
+
if (!target) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
target.options ??= {};
|
|
82
|
+
const styles = target.options['styles'] ?? [];
|
|
83
|
+
for (const style of STYLE_PATHS) {
|
|
84
|
+
const present = styles.some(s => (typeof s === 'string' ? s : s.input) === style);
|
|
85
|
+
if (!present) {
|
|
86
|
+
styles.unshift(style);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
target.options['styles'] = styles;
|
|
90
|
+
const preprocessor = target.options['stylePreprocessorOptions'] ?? {};
|
|
91
|
+
const includePaths = preprocessor.includePaths ?? [];
|
|
92
|
+
if (!includePaths.includes(STYLE_INCLUDE_PATH)) {
|
|
93
|
+
includePaths.push(STYLE_INCLUDE_PATH);
|
|
94
|
+
}
|
|
95
|
+
preprocessor.includePaths = includePaths;
|
|
96
|
+
target.options['stylePreprocessorOptions'] = preprocessor;
|
|
97
|
+
const assets = target.options['assets'] ?? [];
|
|
98
|
+
const servesAssets = assets.some(a => {
|
|
99
|
+
const input = typeof a === 'string' ? a : a.input;
|
|
100
|
+
return input === ASSETS_INPUT;
|
|
101
|
+
});
|
|
102
|
+
if (!servesAssets) {
|
|
103
|
+
assets.push({ glob: '**/*', input: ASSETS_INPUT, output: 'assets' });
|
|
104
|
+
}
|
|
105
|
+
target.options['assets'] = assets;
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
function scaffoldShell(tree, project) {
|
|
109
|
+
return async () => {
|
|
110
|
+
const workspace = await (0, utility_1.readWorkspace)(tree);
|
|
111
|
+
const def = workspace.projects.get(project);
|
|
112
|
+
const sourceRoot = def?.sourceRoot ?? (def ? `${def.root}/src` : 'src');
|
|
113
|
+
const templates = (0, schematics_1.apply)((0, schematics_1.url)('./files'), [
|
|
114
|
+
(0, schematics_1.applyTemplates)({ ...core_1.strings }),
|
|
115
|
+
(0, schematics_1.move)(sourceRoot),
|
|
116
|
+
]);
|
|
117
|
+
// Overwrite the core shell files: ng add assumes a fresh/near-fresh app and the shell is the
|
|
118
|
+
// deliverable. Existing files (e.g. the default app.config.ts from `ng new`) are replaced.
|
|
119
|
+
return (0, schematics_1.mergeWith)(templates, schematics_1.MergeStrategy.Overwrite);
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Copies the Claude skills bundled with the package into the consumer's
|
|
124
|
+
* `.claude/skills` folder. Library-authored skills are authoritative, so existing
|
|
125
|
+
* copies are overwritten to stay in sync with the installed version.
|
|
126
|
+
*/
|
|
127
|
+
function copyClaudeSkills() {
|
|
128
|
+
const skills = (0, schematics_1.apply)((0, schematics_1.url)('./claude-skills'), [(0, schematics_1.move)('.claude/skills')]);
|
|
129
|
+
return (0, schematics_1.mergeWith)(skills, schematics_1.MergeStrategy.Overwrite);
|
|
130
|
+
}
|
|
131
|
+
function logNextSteps(project, options) {
|
|
132
|
+
return (_tree, context) => {
|
|
133
|
+
const log = context.logger;
|
|
134
|
+
log.info('');
|
|
135
|
+
log.info('✅ @open-rlb/ng-app added successfully.');
|
|
136
|
+
log.info(' • Dependencies installed and added to package.json');
|
|
137
|
+
log.info(' • Bootstrap + ng-bootstrap styles registered in angular.json');
|
|
138
|
+
if (!options.skipShell) {
|
|
139
|
+
log.info(` • Application shell scaffolded into the "${project}" app:`);
|
|
140
|
+
log.info(' - src/main.ts bootstraps the AppComponent shell');
|
|
141
|
+
log.info(' - src/environments/environment.ts (config: auth, endpoints, i18n, pages, acl)');
|
|
142
|
+
log.info(' - src/app/app.config.ts (provideRlbConfig + provideApp + RLB_INIT_PROVIDER)');
|
|
143
|
+
log.info(' - src/app/app.component.ts (<rlb-app-container> shell), app.describer.ts, routes, home');
|
|
144
|
+
log.info(' A default root component from `ng new` (e.g. src/app/app.ts) is now unused and can be deleted.');
|
|
145
|
+
}
|
|
146
|
+
if (!options.skipSkills) {
|
|
147
|
+
log.info(' • Claude skills copied to .claude/skills/ (rlb-app-* guides)');
|
|
148
|
+
}
|
|
149
|
+
log.info('');
|
|
150
|
+
log.info('⚠ Before running: edit src/environments/environment.ts and replace the placeholder');
|
|
151
|
+
log.info(' OIDC authority/clientId/redirectUrl and endpoint baseUrls with your real values.');
|
|
152
|
+
log.info(' Then start the app with: ng serve');
|
|
153
|
+
log.info('');
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/** A no-op rule. */
|
|
157
|
+
const noop = tree => tree;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema",
|
|
3
|
+
"$id": "OpenRlbNgAppNgAdd",
|
|
4
|
+
"title": "@open-rlb/ng-app ng-add schematic",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"properties": {
|
|
7
|
+
"project": {
|
|
8
|
+
"type": "string",
|
|
9
|
+
"description": "The name of the project to add the library to.",
|
|
10
|
+
"$default": {
|
|
11
|
+
"$source": "projectName"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"skipShell": {
|
|
15
|
+
"type": "boolean",
|
|
16
|
+
"default": false,
|
|
17
|
+
"description": "Do not scaffold the application shell (environment, providers, app component, routes, home)."
|
|
18
|
+
},
|
|
19
|
+
"skipSkills": {
|
|
20
|
+
"type": "boolean",
|
|
21
|
+
"default": false,
|
|
22
|
+
"description": "Do not copy the bundled Claude skills into .claude/skills."
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"required": []
|
|
26
|
+
}
|