@liorian/sdk 0.29.0-alpha.bdbe320 → 0.31.0-alpha.61fe5c2
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 +1 -1
- package/schemas/module.schema.json +89 -165
- package/src/domain/config/app.config.ts +9 -2
- package/src/domain/entities/catalog.interface.ts +5 -1
- package/src/domain/entities/module-activation.interface.ts +5 -2
- package/src/domain/entities/module.interface.ts +3 -1
- package/src/domain/enums/index.ts +1 -0
- package/src/domain/enums/module-type.enum.ts +51 -0
- package/src/infrastructure/providers/auth.provider.tsx +1 -1
- package/src/infrastructure/utilities/activity-lock.service.ts +22 -7
- package/src/presentation/components/index.tsx +2 -0
- package/src/presentation/components/navigation-progress.tsx +135 -0
- package/src/presentation/components/redirect.tsx +14 -0
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"$id": "module-manifest.schema.json",
|
|
4
4
|
"$comment": "Schéma canonique du manifest.json des modules Liorian (@liorian/sdk). Référencé par les modules du socle (frontend/liorian-socle/src/modules/*) et les modules externes (frontend/liorian-socle/library/modules/*).",
|
|
5
5
|
"title": "Module Manifest (Liorian)",
|
|
6
|
-
"description": "Manifeste d'installation et de déclaration d'un module Liorian. Ce fichier décrit l'identité du module, sa compatibilité (plateformes, versions
|
|
6
|
+
"description": "Manifeste d'installation et de déclaration d'un module Liorian. Ce fichier décrit l'identité du module, sa compatibilité (plateformes, versions socle/API), ses permissions, ses scopes OAuth, ses capacités Tauri et ses prérequis fonctionnels (requirements/optionalRequirements). Les dépendances npm d'exécution et de développement sont portées par le `package.json` du module. Il est la source de vérité des métadonnées techniques d'un module, consommée par le Module Store et l'outillage de distribution.",
|
|
7
7
|
"type": "object",
|
|
8
8
|
"default": {},
|
|
9
9
|
"examples": [
|
|
@@ -16,28 +16,22 @@
|
|
|
16
16
|
"description": "Module d'exemple pour l'onboarding des développeurs",
|
|
17
17
|
"version": "1.0.0",
|
|
18
18
|
"icon": "WandSparklesIcon",
|
|
19
|
-
"type": "
|
|
19
|
+
"type": "WEB_APP_LOCAL",
|
|
20
|
+
"external": false,
|
|
20
21
|
"entry": "index.tsx",
|
|
21
22
|
"uri": "/hello-world",
|
|
22
|
-
"publisher": {
|
|
23
|
-
"id": "liorian",
|
|
24
|
-
"name": "Liorian Workspace"
|
|
25
|
-
},
|
|
26
23
|
"platforms": {
|
|
27
24
|
"web": {"supported": true, "modes": ["web"]},
|
|
28
25
|
"desktop": {"supported": true, "modes": ["local-webview"], "os": ["windows", "macos", "linux"]},
|
|
29
|
-
"mobile": {"supported": true, "modes": ["local-webview"], "os": ["android"]
|
|
26
|
+
"mobile": {"supported": true, "modes": ["local-webview"], "os": ["android"]}
|
|
30
27
|
},
|
|
31
|
-
"
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
"apiScopes": ["hello-world.read", "hello-world.write"],
|
|
35
|
-
"capabilities": {
|
|
36
|
-
"needsNetwork": true,
|
|
37
|
-
"supportsOffline": false,
|
|
38
|
-
"requiresOrganization": true,
|
|
39
|
-
"requiresAuthenticatedUser": true
|
|
28
|
+
"compatibility": {
|
|
29
|
+
"socle": {"min": "0.17.1", "max": "0.17.x"},
|
|
30
|
+
"api": {"min": "0.27.0", "max": "0.27.x"}
|
|
40
31
|
},
|
|
32
|
+
"permissions": ["User:Get", "Editor:Post", "Editor:Put", "Admin:Delete"],
|
|
33
|
+
"oauth": {"scopes": ["openid", "profile", "email", "organizations"]},
|
|
34
|
+
"capabilities": ["core:default"],
|
|
41
35
|
"isEnabled": true,
|
|
42
36
|
"isDefault": false,
|
|
43
37
|
"requirements": {
|
|
@@ -62,12 +56,10 @@
|
|
|
62
56
|
"type",
|
|
63
57
|
"entry",
|
|
64
58
|
"uri",
|
|
65
|
-
"publisher",
|
|
66
59
|
"platforms",
|
|
67
|
-
"
|
|
68
|
-
"apiCompatibility",
|
|
60
|
+
"compatibility",
|
|
69
61
|
"permissions",
|
|
70
|
-
"
|
|
62
|
+
"oauth",
|
|
71
63
|
"capabilities",
|
|
72
64
|
"isEnabled",
|
|
73
65
|
"isDefault",
|
|
@@ -153,10 +145,16 @@
|
|
|
153
145
|
},
|
|
154
146
|
"type": {
|
|
155
147
|
"type": "string",
|
|
156
|
-
"enum": ["
|
|
148
|
+
"enum": ["CONFIGURATION", "EXTERNAL_URL", "WEB_APP_REMOTE", "WEB_APP_CACHED", "WEB_APP_LOCAL", "REMOTE_FRONTEND", "SYSTEM", "SERVICE", "WIDGET", "THEME"],
|
|
157
149
|
"title": "Type de module",
|
|
158
|
-
"description": "
|
|
159
|
-
"default": "
|
|
150
|
+
"description": "Mode de distribution/exécution du module. La distribution indépendante (anciennement `EXTERNAL`) est portée par le booléen `external`.",
|
|
151
|
+
"default": "WEB_APP_LOCAL"
|
|
152
|
+
},
|
|
153
|
+
"external": {
|
|
154
|
+
"type": "boolean",
|
|
155
|
+
"title": "Distribution indépendante",
|
|
156
|
+
"description": "`true` si le module est distribué indépendamment du socle et installable via le Module Store.",
|
|
157
|
+
"default": false
|
|
160
158
|
},
|
|
161
159
|
"entry": {
|
|
162
160
|
"type": "string",
|
|
@@ -181,52 +179,71 @@
|
|
|
181
179
|
"description": "Catégorie du module utilisée pour le filtrage et le regroupement dans le Module Store (alignée sur l'enum `ModuleCategory` du SDK).",
|
|
182
180
|
"examples": ["FINANCE", "COMMERCIAL", "SYSTEM"]
|
|
183
181
|
},
|
|
184
|
-
"publisher": {
|
|
185
|
-
"$ref": "#/definitions/publisher",
|
|
186
|
-
"title": "Éditeur",
|
|
187
|
-
"description": "Identité de l'éditeur du module, affichée dans le Module Store et utilisée pour la vérification de signature."
|
|
188
|
-
},
|
|
189
182
|
"platforms": {
|
|
190
183
|
"$ref": "#/definitions/platforms",
|
|
191
184
|
"title": "Plateformes supportées",
|
|
192
185
|
"description": "Matrice de support du module par plateforme (web, desktop, mobile) et par modes d'exécution."
|
|
193
186
|
},
|
|
194
|
-
"
|
|
195
|
-
"
|
|
196
|
-
"
|
|
197
|
-
"
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
187
|
+
"compatibility": {
|
|
188
|
+
"type": "object",
|
|
189
|
+
"required": ["socle", "api"],
|
|
190
|
+
"properties": {
|
|
191
|
+
"socle": {
|
|
192
|
+
"$ref": "#/definitions/version-range",
|
|
193
|
+
"title": "Compatibilité socle (manager)",
|
|
194
|
+
"description": "Plage de versions du socle (manager, frontend liorian-socle) acceptées par le module. Hors de cette plage, le module est marqué `incompatible`."
|
|
195
|
+
},
|
|
196
|
+
"api": {
|
|
197
|
+
"$ref": "#/definitions/version-range",
|
|
198
|
+
"title": "Compatibilité API",
|
|
199
|
+
"description": "Plage de versions de l'API backend attendues par le module. Hors de cette plage, les appels de service peuvent ne pas être conformes."
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
"additionalProperties": false,
|
|
203
|
+
"title": "Compatibilité",
|
|
204
|
+
"description": "Plages de versions attendues du socle/manager (`socle`) et de l'API backend (`api`). Chaque plage est un objet `version-range` (`min` requis, `max` optionnel).",
|
|
205
|
+
"examples": [
|
|
206
|
+
{
|
|
207
|
+
"socle": {"min": "0.17.1", "max": "0.17.x"},
|
|
208
|
+
"api": {"min": "0.27.0", "max": "0.27.x"}
|
|
209
|
+
}
|
|
210
|
+
]
|
|
203
211
|
},
|
|
204
212
|
"permissions": {
|
|
205
213
|
"type": "array",
|
|
206
214
|
"items": {
|
|
207
|
-
"$ref": "#/definitions/permission-code"
|
|
215
|
+
"$ref": "#/definitions/role-permission-code"
|
|
208
216
|
},
|
|
209
217
|
"minItems": 1,
|
|
210
218
|
"uniqueItems": true,
|
|
211
219
|
"title": "Permissions",
|
|
212
|
-
"description": "
|
|
213
|
-
"examples": [["
|
|
220
|
+
"description": "Accès accordés au module par rôle et par verbe HTTP, au format `<ROLE>:<VERBE>` (ex. `User:Get`, `Editor:Post`, `Editor:Put`, `Admin:Delete`). Rôles et actions alignés sur les rôles/permissions du backend (PermissionsConfig) et sur les verbes HTTP des contrôleurs Raiton.",
|
|
221
|
+
"examples": [["User:Get", "Editor:Post", "Editor:Put", "Admin:Delete"]]
|
|
214
222
|
},
|
|
215
|
-
"
|
|
216
|
-
"type": "
|
|
217
|
-
"
|
|
218
|
-
|
|
223
|
+
"oauth": {
|
|
224
|
+
"type": "object",
|
|
225
|
+
"required": ["scopes"],
|
|
226
|
+
"properties": {
|
|
227
|
+
"scopes": {
|
|
228
|
+
"type": "array",
|
|
229
|
+
"items": {
|
|
230
|
+
"$ref": "#/definitions/oauth-scope"
|
|
231
|
+
},
|
|
232
|
+
"minItems": 1,
|
|
233
|
+
"uniqueItems": true,
|
|
234
|
+
"title": "Scopes OAuth",
|
|
235
|
+
"description": "Scopes OAuth demandés par le module lors de la négociation d'accès. Doivent appartenir au catalogue OAuth autorisé (`openid`, `profile`, `email`, `organizations`, `roles`, `permissions`).",
|
|
236
|
+
"examples": [["openid", "profile", "email", "organizations"]]
|
|
237
|
+
}
|
|
219
238
|
},
|
|
220
|
-
"
|
|
221
|
-
"
|
|
222
|
-
"
|
|
223
|
-
"description": "Scopes OAuth/API recensés par le module, généralement un sous-ensemble des permissions demandées (lecture/écriture). Utilisés lors de la négociation des accès avec l'API backend.",
|
|
224
|
-
"examples": [["accounting.read", "accounting.write"]]
|
|
239
|
+
"additionalProperties": false,
|
|
240
|
+
"title": "Configuration OAuth",
|
|
241
|
+
"description": "Scopes OAuth requis par le module, alignés sur le catalogue de scopes autorisés par le serveur OAuth."
|
|
225
242
|
},
|
|
226
243
|
"capabilities": {
|
|
227
244
|
"$ref": "#/definitions/capabilities",
|
|
228
245
|
"title": "Capacités",
|
|
229
|
-
"description": "
|
|
246
|
+
"description": "Identifiants de permissions/capacités Tauri requis par le module sur le client desktop, alignés sur les capacités déclarées dans `src-tauri/capabilities/`."
|
|
230
247
|
},
|
|
231
248
|
"isEnabled": {
|
|
232
249
|
"type": "boolean",
|
|
@@ -372,7 +389,7 @@
|
|
|
372
389
|
},
|
|
373
390
|
"additionalProperties": false,
|
|
374
391
|
"title": "Plage de compatibilité",
|
|
375
|
-
"description": "Contrainte de compatibilité avec le
|
|
392
|
+
"description": "Contrainte de compatibilité avec le socle (`compatibility.socle`) ou l'API backend (`compatibility.api`) : borne minimale requise, borne maximale et sévérité.",
|
|
376
393
|
"examples": [{"min": "0.17.1", "max": "0.17.x"}]
|
|
377
394
|
},
|
|
378
395
|
"module-version-map": {
|
|
@@ -390,52 +407,6 @@
|
|
|
390
407
|
"description": "Association d'identifiants de modules (`requirements` / `optionalRequirements`) à des plages de versions acceptées.",
|
|
391
408
|
"examples": [{"organization": ">=1.0.0", "media-library": ">=1.0.0"}]
|
|
392
409
|
},
|
|
393
|
-
"publisher": {
|
|
394
|
-
"type": "object",
|
|
395
|
-
"required": ["id", "name"],
|
|
396
|
-
"properties": {
|
|
397
|
-
"id": {
|
|
398
|
-
"type": "string",
|
|
399
|
-
"pattern": "^[a-z0-9]+([-_][a-z0-9]+)*$",
|
|
400
|
-
"title": "Identifiant de l'éditeur",
|
|
401
|
-
"description": "Identifiant technique de l'éditeur (ex. `liorian`)."
|
|
402
|
-
},
|
|
403
|
-
"name": {
|
|
404
|
-
"type": "string",
|
|
405
|
-
"minLength": 1,
|
|
406
|
-
"title": "Nom de l'éditeur",
|
|
407
|
-
"description": "Nom affiché de l'éditeur (ex. `Liorian Workspace`)."
|
|
408
|
-
},
|
|
409
|
-
"url": {
|
|
410
|
-
"type": "string",
|
|
411
|
-
"format": "uri",
|
|
412
|
-
"title": "Site de l'éditeur",
|
|
413
|
-
"description": "URL du site officiel de l'éditeur."
|
|
414
|
-
},
|
|
415
|
-
"email": {
|
|
416
|
-
"type": "string",
|
|
417
|
-
"format": "email",
|
|
418
|
-
"title": "Contact",
|
|
419
|
-
"description": "Adresse de contact du support."
|
|
420
|
-
},
|
|
421
|
-
"description": {
|
|
422
|
-
"type": "string",
|
|
423
|
-
"maxLength": 256,
|
|
424
|
-
"title": "Description",
|
|
425
|
-
"description": "Courte présentation de l'éditeur."
|
|
426
|
-
},
|
|
427
|
-
"avatar": {
|
|
428
|
-
"type": "string",
|
|
429
|
-
"format": "uri",
|
|
430
|
-
"title": "Avatar",
|
|
431
|
-
"description": "URL de l'avatar logo de l'éditeur."
|
|
432
|
-
}
|
|
433
|
-
},
|
|
434
|
-
"additionalProperties": false,
|
|
435
|
-
"title": "Éditeur",
|
|
436
|
-
"description": "Identité de l'entité publiant le module.",
|
|
437
|
-
"examples": [{"id": "liorian", "name": "Liorian Workspace"}]
|
|
438
|
-
},
|
|
439
410
|
"platforms": {
|
|
440
411
|
"type": "object",
|
|
441
412
|
"required": ["web", "desktop", "mobile"],
|
|
@@ -494,14 +465,9 @@
|
|
|
494
465
|
},
|
|
495
466
|
"uniqueItems": true,
|
|
496
467
|
"title": "Systèmes d'exploitation",
|
|
497
|
-
"description": "Systèmes d'exploitation supportés (pertinent pour desktop/mobile).",
|
|
468
|
+
"description": "Systèmes d'exploitation supportés (pertinent pour desktop/mobile, où `ios` signifie support iOS).",
|
|
498
469
|
"examples": [["windows", "macos", "linux"]]
|
|
499
470
|
},
|
|
500
|
-
"iosSupported": {
|
|
501
|
-
"type": "boolean",
|
|
502
|
-
"title": "Support iOS",
|
|
503
|
-
"description": "Indique si iOS est supporté sur la plateforme mobile (obsolète si `os` inclut déjà `ios`)."
|
|
504
|
-
},
|
|
505
471
|
"minOsVersion": {
|
|
506
472
|
"type": "string",
|
|
507
473
|
"title": "Version OS minimale",
|
|
@@ -529,71 +495,29 @@
|
|
|
529
495
|
"description": "Définition du support d'un module sur une plateforme donnée."
|
|
530
496
|
},
|
|
531
497
|
"capabilities": {
|
|
532
|
-
"type": "
|
|
533
|
-
"
|
|
534
|
-
"
|
|
535
|
-
|
|
536
|
-
"default": true,
|
|
537
|
-
"title": "Réseau requis",
|
|
538
|
-
"description": "Si `true`, le module requiert une connexion réseau pour fonctionner (appels API)."
|
|
539
|
-
},
|
|
540
|
-
"supportsOffline": {
|
|
541
|
-
"type": "boolean",
|
|
542
|
-
"default": false,
|
|
543
|
-
"title": "Mode hors-ligne",
|
|
544
|
-
"description": "Si `true`, le module conserve un fonctionnement dégradé hors connexion (cache local, file d'attente)."
|
|
545
|
-
},
|
|
546
|
-
"requiresOrganization": {
|
|
547
|
-
"type": "boolean",
|
|
548
|
-
"default": true,
|
|
549
|
-
"title": "Organisation requise",
|
|
550
|
-
"description": "Si `true`, le module exige une organisation active (le Module Guard bloque sans organisation)."
|
|
551
|
-
},
|
|
552
|
-
"requiresAuthenticatedUser": {
|
|
553
|
-
"type": "boolean",
|
|
554
|
-
"default": true,
|
|
555
|
-
"title": "Utilisateur authentifié requis",
|
|
556
|
-
"description": "Si `true`, le module exige une session utilisateur authentifiée."
|
|
557
|
-
},
|
|
558
|
-
"requiresAdmin": {
|
|
559
|
-
"type": "boolean",
|
|
560
|
-
"default": false,
|
|
561
|
-
"title": "Administrateur requis",
|
|
562
|
-
"description": "Si `true`, seules les sessions avec le rôle administrateur peuvent accéder au module."
|
|
563
|
-
},
|
|
564
|
-
"supportsRealtime": {
|
|
565
|
-
"type": "boolean",
|
|
566
|
-
"default": false,
|
|
567
|
-
"title": "Temps réel",
|
|
568
|
-
"description": "Si `true`, le module s'appuie sur des routines et/ou événements temps réel (push, websocket)."
|
|
569
|
-
},
|
|
570
|
-
"processesLocalData": {
|
|
571
|
-
"type": "boolean",
|
|
572
|
-
"default": false,
|
|
573
|
-
"title": "Traitement local des données",
|
|
574
|
-
"description": "Si `true`, le module traite des données sur l'appareil (chiffrement local, médias locaux)."
|
|
575
|
-
}
|
|
498
|
+
"type": "array",
|
|
499
|
+
"items": {
|
|
500
|
+
"type": "string",
|
|
501
|
+
"pattern": "^[a-z][a-z0-9_]*:[a-z0-9_.:-]+$"
|
|
576
502
|
},
|
|
577
|
-
"
|
|
578
|
-
"title": "Capacités
|
|
579
|
-
"description": "
|
|
580
|
-
"examples": [
|
|
581
|
-
{
|
|
582
|
-
"needsNetwork": true,
|
|
583
|
-
"supportsOffline": false,
|
|
584
|
-
"requiresOrganization": true,
|
|
585
|
-
"requiresAuthenticatedUser": true
|
|
586
|
-
}
|
|
587
|
-
]
|
|
503
|
+
"uniqueItems": true,
|
|
504
|
+
"title": "Capacités Tauri",
|
|
505
|
+
"description": "Identifiants de permissions/capacités Tauri requis par le module sur le client desktop. Valeurs alignées sur les identifiants déclarés dans `src-tauri/capabilities/*.json` (ex. `core:default`, `notification:default`, `core:window:allow-start-dragging`).",
|
|
506
|
+
"examples": [["core:default", "notification:default"]]
|
|
588
507
|
},
|
|
589
|
-
"permission-code": {
|
|
508
|
+
"role-permission-code": {
|
|
590
509
|
"type": "string",
|
|
591
|
-
"pattern": "^
|
|
592
|
-
"
|
|
593
|
-
"
|
|
594
|
-
"
|
|
595
|
-
|
|
596
|
-
|
|
510
|
+
"pattern": "^(Root|SuperAdmin|Admin|Manager|Editor|Moderator|Analyst|Commercial|Operator|Contributor|Caissier|User|Guest|Viewer):(Get|Post|Put|Delete)$",
|
|
511
|
+
"title": "Permission rôle:verbe",
|
|
512
|
+
"description": "Accès accordé au format `<ROLE>:<VERBE>`. `ROLE` est un nom de rôle existant du backend (`Root`, `SuperAdmin`, `Admin`, `Manager`, `Editor`, `Moderator`, `Analyst`, `Commercial`, `Operator`, `Contributor`, `Caissier`, `User`, `Guest`, `Viewer`) et `VERBE` un verbe HTTP (`Get`, `Post`, `Put`, `Delete`).",
|
|
513
|
+
"examples": ["User:Get", "Editor:Post", "Editor:Put", "Admin:Delete"]
|
|
514
|
+
},
|
|
515
|
+
"oauth-scope": {
|
|
516
|
+
"type": "string",
|
|
517
|
+
"enum": ["openid", "profile", "email", "organizations", "roles", "permissions"],
|
|
518
|
+
"title": "Scope OAuth",
|
|
519
|
+
"description": "Scope OAuth du catalogue autorisé, aligné sur les scopes pré-enregistrés du serveur OAuth (définis par `packages/oauth-kit` et les seeds).",
|
|
520
|
+
"examples": ["openid", "profile", "email", "organizations"]
|
|
597
521
|
},
|
|
598
522
|
"icon-reference": {
|
|
599
523
|
"type": "string",
|
|
@@ -11,9 +11,16 @@ export const AppConfig = {
|
|
|
11
11
|
APP_VERSION: process.env.NEXT_PUBLIC_APP_VERSION as unknown as string,
|
|
12
12
|
APP_VERSION_NUMBER: process.env.NEXT_PUBLIC_APP_VERSION_NUMBER as unknown as number,
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
/** Alias conservé pour compatibilité : renvoie la même cadence que `APP_REFRESH_UI`. */
|
|
15
|
+
UI_REFRESH_TIMEOUT: toPositiveMilliseconds(
|
|
16
|
+
process.env.NEXT_PUBLIC_APP_REFRESH_UI ?? process.env.NEXT_PUBLIC_UI_REFRESH_TIMEOUT,
|
|
17
|
+
30 * 1000
|
|
18
|
+
),
|
|
15
19
|
UI_ACTIVITY_TRACKER_TIMEOUT: toPositiveMilliseconds(process.env.NEXT_PUBLIC_UI_ACTIVITY_TRACKER_TIMEOUT, 3 * 60 * 1000),
|
|
16
|
-
APP_REFRESH_UI: toPositiveMilliseconds(
|
|
20
|
+
APP_REFRESH_UI: toPositiveMilliseconds(
|
|
21
|
+
process.env.NEXT_PUBLIC_APP_REFRESH_UI ?? process.env.NEXT_PUBLIC_UI_REFRESH_TIMEOUT,
|
|
22
|
+
30 * 1000
|
|
23
|
+
),
|
|
17
24
|
|
|
18
25
|
AUTH_HOST: process.env.NEXT_PUBLIC_AUTH_HOST as unknown as string,
|
|
19
26
|
CONNECT_HOST: process.env.NEXT_PUBLIC_CONNECT_HOST as unknown as string,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type {ModuleCategory} from "../enums/module-category.enum";
|
|
2
|
+
import type {CatalogModuleType} from "../enums/module-type.enum";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Types du catalogue public (storefront).
|
|
@@ -9,7 +10,7 @@ import type {ModuleCategory} from "../enums/module-category.enum";
|
|
|
9
10
|
* inter-service.
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
|
-
export type CatalogModuleType
|
|
13
|
+
export type {CatalogModuleType} from "../enums/module-type.enum";
|
|
13
14
|
|
|
14
15
|
export interface CatalogCategoryInterface {
|
|
15
16
|
id: string;
|
|
@@ -37,6 +38,7 @@ export interface CatalogModuleInterface {
|
|
|
37
38
|
isEnabled: boolean;
|
|
38
39
|
isDefault: boolean;
|
|
39
40
|
type: CatalogModuleType;
|
|
41
|
+
external: boolean;
|
|
40
42
|
categoryId?: string | null;
|
|
41
43
|
category?: keyof typeof ModuleCategory | null;
|
|
42
44
|
editorId?: string | null;
|
|
@@ -55,6 +57,7 @@ export interface CreateCatalogModulePayloadInterface {
|
|
|
55
57
|
isEnabled?: boolean;
|
|
56
58
|
isDefault?: boolean;
|
|
57
59
|
type?: CatalogModuleType;
|
|
60
|
+
external?: boolean;
|
|
58
61
|
categoryId?: string | null;
|
|
59
62
|
}
|
|
60
63
|
|
|
@@ -69,6 +72,7 @@ export interface UpdateCatalogModulePayloadInterface {
|
|
|
69
72
|
isEnabled?: boolean;
|
|
70
73
|
isDefault?: boolean;
|
|
71
74
|
type?: CatalogModuleType;
|
|
75
|
+
external?: boolean;
|
|
72
76
|
categoryId?: string | null;
|
|
73
77
|
}
|
|
74
78
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {ModuleCategory} from "../enums/module-category.enum";
|
|
2
|
+
import type {ModuleType} from "../enums/module-type.enum";
|
|
2
3
|
|
|
3
4
|
export interface ModuleSerialKeyInterface {
|
|
4
5
|
id: string;
|
|
@@ -97,7 +98,8 @@ export interface ModuleStoreSyncPayloadInterface {
|
|
|
97
98
|
version?: string;
|
|
98
99
|
enabled: boolean;
|
|
99
100
|
checksum?: string;
|
|
100
|
-
type?:
|
|
101
|
+
type?: ModuleType;
|
|
102
|
+
external?: boolean;
|
|
101
103
|
}[];
|
|
102
104
|
}
|
|
103
105
|
|
|
@@ -157,7 +159,8 @@ export interface ModuleRegistryInstalledModuleInterface {
|
|
|
157
159
|
key?: string;
|
|
158
160
|
name: string;
|
|
159
161
|
version: string;
|
|
160
|
-
type:
|
|
162
|
+
type: ModuleType;
|
|
163
|
+
external?: boolean;
|
|
161
164
|
category?: keyof typeof ModuleCategory;
|
|
162
165
|
enabled: boolean;
|
|
163
166
|
state: ModuleStoreStateType;
|
|
@@ -5,8 +5,9 @@ import {RoutineInterface} from "../typing/routine.types";
|
|
|
5
5
|
import {AppRouterInstance} from "next/dist/shared/lib/app-router-context.shared-runtime";
|
|
6
6
|
import {ModuleStoreStateType, ModuleConfigSettingsFieldInterface} from "./module-activation.interface";
|
|
7
7
|
import {ModuleCategory} from "../enums/module-category.enum";
|
|
8
|
+
import type {ModuleType} from "../enums/module-type.enum";
|
|
8
9
|
|
|
9
|
-
export type ModuleType
|
|
10
|
+
export type {ModuleType} from "../enums/module-type.enum";
|
|
10
11
|
|
|
11
12
|
export interface ModuleWidgetsInterface {
|
|
12
13
|
[K: string]: React.ComponentType<any>;
|
|
@@ -46,6 +47,7 @@ export interface ModuleDeclarationInterface {
|
|
|
46
47
|
isEnabled?: boolean;
|
|
47
48
|
isDefault?: boolean; // Si true, ne peut pas être désactivé ou déplacé
|
|
48
49
|
type: ModuleType;
|
|
50
|
+
external: boolean; // Si true, module distribué indépendamment du socle
|
|
49
51
|
menu?: ModuleNavigationMenu;
|
|
50
52
|
requirements?: Record<string, string>; // Modules nécessaires pour fonctionner. Ex: 'nom-du-module' : 'version'
|
|
51
53
|
optionalRequirements?: Record<string, string>; // Modules nécessaires pour fonctionner. Ex: 'nom-du-module' : 'version'
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type de module (mode de distribution / d'exécution).
|
|
3
|
+
*
|
|
4
|
+
* La distribution indépendante (anciennement `EXTERNAL`) est portée par le
|
|
5
|
+
* booléen `external` de la déclaration de module.
|
|
6
|
+
*/
|
|
7
|
+
export const ModuleType = {
|
|
8
|
+
CONFIGURATION: 'CONFIGURATION',
|
|
9
|
+
EXTERNAL_URL: 'EXTERNAL_URL',
|
|
10
|
+
WEB_APP_REMOTE: 'WEB_APP_REMOTE',
|
|
11
|
+
WEB_APP_CACHED: 'WEB_APP_CACHED',
|
|
12
|
+
WEB_APP_LOCAL: 'WEB_APP_LOCAL',
|
|
13
|
+
REMOTE_FRONTEND: 'REMOTE_FRONTEND',
|
|
14
|
+
SYSTEM: 'SYSTEM',
|
|
15
|
+
SERVICE: 'SERVICE',
|
|
16
|
+
WIDGET: 'WIDGET',
|
|
17
|
+
THEME: 'THEME',
|
|
18
|
+
} as const;
|
|
19
|
+
|
|
20
|
+
export type ModuleType = (typeof ModuleType)[keyof typeof ModuleType];
|
|
21
|
+
|
|
22
|
+
/** Ordre d'affichage canonique des types de module. */
|
|
23
|
+
export const MODULE_TYPE_ORDER: ModuleType[] = [
|
|
24
|
+
ModuleType.CONFIGURATION,
|
|
25
|
+
ModuleType.EXTERNAL_URL,
|
|
26
|
+
ModuleType.WEB_APP_REMOTE,
|
|
27
|
+
ModuleType.WEB_APP_CACHED,
|
|
28
|
+
ModuleType.WEB_APP_LOCAL,
|
|
29
|
+
ModuleType.REMOTE_FRONTEND,
|
|
30
|
+
ModuleType.SYSTEM,
|
|
31
|
+
ModuleType.SERVICE,
|
|
32
|
+
ModuleType.WIDGET,
|
|
33
|
+
ModuleType.THEME,
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
/** Libellés français des types de module. */
|
|
37
|
+
export const MODULE_TYPE_LABELS: Record<ModuleType, string> = {
|
|
38
|
+
CONFIGURATION: 'Configuration',
|
|
39
|
+
EXTERNAL_URL: 'URL externe',
|
|
40
|
+
WEB_APP_REMOTE: 'Application web distante',
|
|
41
|
+
WEB_APP_CACHED: 'Application web mise en cache',
|
|
42
|
+
WEB_APP_LOCAL: 'Application web locale',
|
|
43
|
+
REMOTE_FRONTEND: 'Frontend distant',
|
|
44
|
+
SYSTEM: 'Système',
|
|
45
|
+
SERVICE: 'Service',
|
|
46
|
+
WIDGET: 'Widget',
|
|
47
|
+
THEME: 'Thème',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** Alias conservé pour le catalogue public (`CatalogModuleType`). */
|
|
51
|
+
export type CatalogModuleType = ModuleType;
|
|
@@ -46,7 +46,7 @@ export function AuthProvider({children, requireOrganization = true}: AuthProvide
|
|
|
46
46
|
if (!armed()) return
|
|
47
47
|
ActivityLockService.start({
|
|
48
48
|
userId,
|
|
49
|
-
timeout:
|
|
49
|
+
timeout: AppConfig.UI_ACTIVITY_TRACKER_TIMEOUT,
|
|
50
50
|
onLocked: () => setLocked(true),
|
|
51
51
|
})
|
|
52
52
|
})
|
|
@@ -12,9 +12,6 @@ export const ACTIVITY_LOCK_PREFERENCE_LABEL = "ui_activity_tracker_timeout";
|
|
|
12
12
|
/** Clé localStorage du verrou : persiste l'état verrouillé même après un rafraîchissement. */
|
|
13
13
|
const ACTIVITY_LOCK_STORAGE_KEY = "@liorian-activity-lock";
|
|
14
14
|
|
|
15
|
-
/** Délai d'inactivité par défaut (repli) : 3 minutes. */
|
|
16
|
-
const DEFAULT_TIMEOUT = 3 * 60 * 1000;
|
|
17
|
-
|
|
18
15
|
export interface ActivityLockState {
|
|
19
16
|
locked: boolean;
|
|
20
17
|
userId?: string;
|
|
@@ -115,6 +112,11 @@ export class ActivityLockService {
|
|
|
115
112
|
/**
|
|
116
113
|
* Résout le délai d'inactivité : préférence utilisateur persistée (si définie),
|
|
117
114
|
* sinon `AppConfig.UI_ACTIVITY_TRACKER_TIMEOUT`, sinon valeur par défaut.
|
|
115
|
+
*
|
|
116
|
+
* La préférence utilisateur prime volontairement sur la configuration
|
|
117
|
+
* `NEXT_PUBLIC_UI_ACTIVITY_TRACKER_TIMEOUT` : c'est le comportement attendu,
|
|
118
|
+
* mais une trace (mode développement) indique la source réellement appliquée
|
|
119
|
+
* pour lever toute ambiguïté.
|
|
118
120
|
*/
|
|
119
121
|
static async getTimeout(userId?: string): Promise<number> {
|
|
120
122
|
if (userId) {
|
|
@@ -122,10 +124,23 @@ export class ActivityLockService {
|
|
|
122
124
|
.getValue(userId, ACTIVITY_LOCK_PREFERENCE_LABEL)
|
|
123
125
|
.catch(() => undefined);
|
|
124
126
|
const parsed = raw ? Number(raw) : NaN;
|
|
125
|
-
if (Number.isFinite(parsed) && parsed > 0)
|
|
127
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
128
|
+
if (process.env.NODE_ENV === "development") {
|
|
129
|
+
console.info(
|
|
130
|
+
`[ActivityLock] Préférence utilisateur "${ACTIVITY_LOCK_PREFERENCE_LABEL}" = ${parsed} ms —` +
|
|
131
|
+
"elle prime sur NEXT_PUBLIC_UI_ACTIVITY_TRACKER_TIMEOUT."
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
return parsed;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (process.env.NODE_ENV === "development") {
|
|
138
|
+
console.info(
|
|
139
|
+
`[ActivityLock] Délai d'inactivité = ${AppConfig.UI_ACTIVITY_TRACKER_TIMEOUT} ms` +
|
|
140
|
+
"(NEXT_PUBLIC_UI_ACTIVITY_TRACKER_TIMEOUT)."
|
|
141
|
+
);
|
|
126
142
|
}
|
|
127
|
-
|
|
128
|
-
return Number.isFinite(config) && config > 0 ? config : DEFAULT_TIMEOUT;
|
|
143
|
+
return AppConfig.UI_ACTIVITY_TRACKER_TIMEOUT;
|
|
129
144
|
}
|
|
130
145
|
|
|
131
146
|
/** Persiste le délai d'inactivité choisi par l'utilisateur (via UserPreference). */
|
|
@@ -149,7 +164,7 @@ export class ActivityLockService {
|
|
|
149
164
|
|
|
150
165
|
this.enabled = true;
|
|
151
166
|
this.userId = options.userId;
|
|
152
|
-
this.timeout = options.timeout ??
|
|
167
|
+
this.timeout = options.timeout ?? AppConfig.UI_ACTIVITY_TRACKER_TIMEOUT;
|
|
153
168
|
this.onLocked = options.onLocked;
|
|
154
169
|
|
|
155
170
|
// S'assure que le tracker d'activité tourne (partagé avec les notifications push) :
|
|
@@ -16,6 +16,7 @@ export * from "./local-time";
|
|
|
16
16
|
export * from "./markdown-parser";
|
|
17
17
|
export * from "./message-scroller";
|
|
18
18
|
export * from "./module-first-access-config.view";
|
|
19
|
+
export * from "./navigation-progress";
|
|
19
20
|
export * from "./module-first-access-pending.view";
|
|
20
21
|
export * from "./module-guard";
|
|
21
22
|
export * from "./modules-first-access.guard";
|
|
@@ -24,6 +25,7 @@ export * from "./notification-details.modal";
|
|
|
24
25
|
export * from "./notification-item";
|
|
25
26
|
export * from "./notifications-popover";
|
|
26
27
|
export * from "./push-notifications.provider";
|
|
28
|
+
export * from "./redirect";
|
|
27
29
|
export * from "./upload-progress-dialog";
|
|
28
30
|
export * from "./waiting";
|
|
29
31
|
export * from "./waiting-bar";
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import {usePathname} from "next/navigation";
|
|
5
|
+
import {useIsFetching} from "@tanstack/react-query";
|
|
6
|
+
import {WaitingBar} from "./waiting-bar";
|
|
7
|
+
import {cn} from "../../infrastructure/utilities/utils";
|
|
8
|
+
|
|
9
|
+
// Délai avant affichage : évite tout flash sur les navigations instantanées
|
|
10
|
+
const SHOW_DELAY_MS = 150;
|
|
11
|
+
// Filet de sécurité : la barre ne doit jamais rester bloquée à l'écran
|
|
12
|
+
const SAFETY_TIMEOUT_MS = 20_000;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
Barre de progression de navigation globale, visible :
|
|
16
|
+
- dès que l'utilisateur entame un changement de page (clic sur un lien
|
|
17
|
+
interne ou navigation programmatique),
|
|
18
|
+
- pendant que le serveur compile la page demandée (dev, compilation à la
|
|
19
|
+
demande : le clic bloque jusqu'à la fin de compilation),
|
|
20
|
+
puis masquée dès que la page cible est affichée.
|
|
21
|
+
S'affiche aussi pendant les requêtes react-query en cours (comportement
|
|
22
|
+
historique).
|
|
23
|
+
*/
|
|
24
|
+
export function NavigationProgress() {
|
|
25
|
+
const isFetching = useIsFetching() > 0;
|
|
26
|
+
|
|
27
|
+
const [navigating, setNavigating] = React.useState(false);
|
|
28
|
+
const pathname = usePathname();
|
|
29
|
+
|
|
30
|
+
const targetRef = React.useRef<string | null>(null);
|
|
31
|
+
const showTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
32
|
+
const safetyTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
33
|
+
|
|
34
|
+
const finish = React.useCallback(() => {
|
|
35
|
+
if (showTimerRef.current) clearTimeout(showTimerRef.current);
|
|
36
|
+
if (safetyTimerRef.current) clearTimeout(safetyTimerRef.current);
|
|
37
|
+
showTimerRef.current = null;
|
|
38
|
+
safetyTimerRef.current = null;
|
|
39
|
+
targetRef.current = null;
|
|
40
|
+
setNavigating(false);
|
|
41
|
+
}, []);
|
|
42
|
+
|
|
43
|
+
React.useEffect(() => {
|
|
44
|
+
const currentPath = () => window.location.pathname + window.location.search;
|
|
45
|
+
|
|
46
|
+
const startNavigation = (path: string) => {
|
|
47
|
+
if (path === currentPath()) return;
|
|
48
|
+
targetRef.current = path;
|
|
49
|
+
if (showTimerRef.current) clearTimeout(showTimerRef.current);
|
|
50
|
+
showTimerRef.current = setTimeout(() => setNavigating(true), SHOW_DELAY_MS);
|
|
51
|
+
if (safetyTimerRef.current) clearTimeout(safetyTimerRef.current);
|
|
52
|
+
safetyTimerRef.current = setTimeout(finish, SAFETY_TIMEOUT_MS);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// 1. Clic sur un lien interne : signal immédiat, y compris pendant que
|
|
56
|
+
// le serveur compile la page suivante (aucun événement routeur tant
|
|
57
|
+
// que la compilation/RSC n'est pas prête).
|
|
58
|
+
const onClickCapture = (event: MouseEvent) => {
|
|
59
|
+
if (event.defaultPrevented || event.button !== 0 ||
|
|
60
|
+
event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
|
|
61
|
+
const anchor = (event.target as Element | null)?.closest?.("a");
|
|
62
|
+
if (!anchor) return;
|
|
63
|
+
const href = anchor.getAttribute("href");
|
|
64
|
+
if (!href || href.startsWith("#")) return;
|
|
65
|
+
if (anchor.target === "_blank" || anchor.hasAttribute("download")) return;
|
|
66
|
+
let url: URL;
|
|
67
|
+
try {
|
|
68
|
+
url = new URL(anchor.href, window.location.href);
|
|
69
|
+
} catch {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (url.origin !== window.location.origin) return;
|
|
73
|
+
startNavigation(url.pathname + url.search);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// 2. Navigations programmatiques (router.push…) : le routeur passe par
|
|
77
|
+
// l'History API au moment où il confirme la navigation.
|
|
78
|
+
const wrap = (key: "pushState" | "replaceState") => {
|
|
79
|
+
const original = window.history[key].bind(window.history);
|
|
80
|
+
(window.history as any)[key] = (data: unknown, unused: string, url?: string | URL | null) => {
|
|
81
|
+
try {
|
|
82
|
+
if (url != null) {
|
|
83
|
+
const parsed = new URL(String(url), window.location.href);
|
|
84
|
+
if (parsed.origin === window.location.origin) {
|
|
85
|
+
startNavigation(parsed.pathname + parsed.search);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
} catch {
|
|
89
|
+
// URL non parsable : ignorer silencieusement
|
|
90
|
+
}
|
|
91
|
+
const result = original(data, unused, url as never);
|
|
92
|
+
// Navigation confirmée : masque dès que la cible est atteinte
|
|
93
|
+
if (targetRef.current && currentPath() === targetRef.current) {
|
|
94
|
+
setTimeout(finish, 0);
|
|
95
|
+
}
|
|
96
|
+
return result;
|
|
97
|
+
};
|
|
98
|
+
return original;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const originalPush = wrap("pushState");
|
|
102
|
+
const originalReplace = wrap("replaceState");
|
|
103
|
+
|
|
104
|
+
document.addEventListener("click", onClickCapture, true);
|
|
105
|
+
return () => {
|
|
106
|
+
document.removeEventListener("click", onClickCapture, true);
|
|
107
|
+
window.history.pushState = originalPush;
|
|
108
|
+
window.history.replaceState = originalReplace;
|
|
109
|
+
if (showTimerRef.current) clearTimeout(showTimerRef.current);
|
|
110
|
+
if (safetyTimerRef.current) clearTimeout(safetyTimerRef.current);
|
|
111
|
+
};
|
|
112
|
+
}, [finish]);
|
|
113
|
+
|
|
114
|
+
// Filet complémentaire : la page cible est rendue (changement de route
|
|
115
|
+
// détecté par le routeur) alors que le push n'aurait pas suffi.
|
|
116
|
+
React.useEffect(() => {
|
|
117
|
+
if (targetRef.current && window.location.pathname + window.location.search === targetRef.current) {
|
|
118
|
+
finish();
|
|
119
|
+
}
|
|
120
|
+
}, [pathname, finish]);
|
|
121
|
+
|
|
122
|
+
const visible = navigating || isFetching;
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<div
|
|
126
|
+
aria-hidden={!visible}
|
|
127
|
+
className={cn(
|
|
128
|
+
"fixed top-0 left-0 right-0 z-9999 transition-opacity duration-300",
|
|
129
|
+
visible ? "opacity-100" : "opacity-0 pointer-events-none",
|
|
130
|
+
)}
|
|
131
|
+
>
|
|
132
|
+
<WaitingBar/>
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import {useEffect} from "react";
|
|
4
|
+
import {useRouter} from "next/navigation";
|
|
5
|
+
|
|
6
|
+
export function Redirect({to}: { to: string }) {
|
|
7
|
+
const router = useRouter();
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
router.replace(to);
|
|
11
|
+
}, [router, to]);
|
|
12
|
+
|
|
13
|
+
return null;
|
|
14
|
+
}
|