goblin-laboratory 4.10.0 → 4.11.1
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/README.md +538 -60
- package/config.js +1 -8
- package/package.json +3 -3
- package/widgets/connect-helpers/map-c.js +17 -0
- package/widgets/connect-helpers/with-c.js +15 -2
- package/widgets/laboratory/service.js +80 -88
- package/widgets/widget/index.js +12 -2
package/README.md
CHANGED
|
@@ -1,107 +1,585 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 📘 goblin-laboratory
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## Aperçu
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
`goblin-laboratory` est le module central de l'écosystème Xcraft pour la construction d'interfaces utilisateur React. Il orchestre la création de fenêtres applicatives (via Electron ou WebSocket), gère le cycle de vie des widgets connectés au store Redux, assure la synchronisation des états backend vers le frontend, et fournit la classe de base `Widget` dont dérivent tous les composants graphiques Xcraft.
|
|
6
6
|
|
|
7
|
-
Il
|
|
8
|
-
en ouvrant le bundle webpack à une URL donnée.
|
|
7
|
+
Le laboratoire est l'acteur Goblin qui fait le lien entre le monde backend (bus Xcraft, acteurs Goblin/Elf) et le monde frontend (React, Redux). Il gère également le thème visuel, le zoom de l'interface, et expose un terminal intégré (`Termux`) pour l'administration.
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
## Sommaire
|
|
11
10
|
|
|
12
|
-
|
|
11
|
+
- [Aperçu](#aperçu)
|
|
12
|
+
- [Structure du module](#structure-du-module)
|
|
13
|
+
- [Fonctionnement global](#fonctionnement-global)
|
|
14
|
+
- [Exemples d'utilisation](#exemples-dutilisation)
|
|
15
|
+
- [Interactions avec d'autres modules](#interactions-avec-dautres-modules)
|
|
16
|
+
- [Configuration avancée](#configuration-avancée)
|
|
17
|
+
- [Détails des sources](#détails-des-sources)
|
|
18
|
+
- [Licence](#licence)
|
|
13
19
|
|
|
14
|
-
|
|
20
|
+
## Structure du module
|
|
15
21
|
|
|
16
|
-
|
|
17
|
-
- top-bar
|
|
18
|
-
- task-bar
|
|
19
|
-
- content
|
|
22
|
+
Le module s'articule autour de plusieurs couches :
|
|
20
23
|
|
|
21
|
-
|
|
24
|
+
**Acteurs backend (Goblin/Elf) :**
|
|
22
25
|
|
|
23
|
-
|
|
26
|
+
- `laboratory` — acteur Goblin principal, gère la fenêtre et le cycle de vie de l'interface
|
|
27
|
+
- `carnotzet` — acteur Goblin léger pour les interfaces sans fenêtre native (mode WebSocket pur)
|
|
28
|
+
- `Termux` — acteur Elf singleton, terminal de commandes intégré à l'interface
|
|
29
|
+
- `Blueprint` / `Blueprints` — acteurs Elf pour la persistance de métadonnées d'entités
|
|
24
30
|
|
|
25
|
-
|
|
31
|
+
**Infrastructure frontend :**
|
|
26
32
|
|
|
27
|
-
|
|
33
|
+
- `Renderer` et ses variantes (`ElectronRenderer`, `BrowsersRenderer`, `ElectronRendererWS`) — bootstrapping React
|
|
34
|
+
- `Widget` — classe de base pour tous les composants graphiques Xcraft
|
|
35
|
+
- Redux store avec ses reducers (`backend`, `widgets`, `commands`, `network`, `app`, `router`)
|
|
36
|
+
- Middlewares Redux (`transit`, `quest`, `form`) pour la communication bidirectionnelle
|
|
28
37
|
|
|
29
|
-
|
|
38
|
+
**Widgets utilitaires :**
|
|
30
39
|
|
|
31
|
-
|
|
40
|
+
- `ThemeContext` — injection du thème CSS calculé dynamiquement
|
|
41
|
+
- `WithModel`, `WithC`, `C` — connexion déclarative des props au state Redux
|
|
42
|
+
- `StateLoader`, `CollectionLoader` — chargement conditionnel selon l'état backend
|
|
43
|
+
- `ErrorHandler` — boundary d'erreurs React avec récupération
|
|
44
|
+
- `DisconnectOverlay`, `Maintenance` — états de connexion et maintenance
|
|
45
|
+
- `Frame`, `Root` — racine de l'arbre React
|
|
32
46
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
47
|
+
## Fonctionnement global
|
|
48
|
+
|
|
49
|
+
### Flux de données backend → frontend
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
Backend (Goblin/Elf)
|
|
53
|
+
│
|
|
54
|
+
▼
|
|
55
|
+
Warehouse (état)
|
|
56
|
+
│
|
|
57
|
+
▼
|
|
58
|
+
Channel (ElectronChannel / WebSocketChannel)
|
|
59
|
+
│ envoie NEW_BACKEND_STATE / PUSH_PATH / DISPATCH_IN_APP
|
|
60
|
+
▼
|
|
61
|
+
Renderer (frontend)
|
|
62
|
+
│
|
|
63
|
+
▼
|
|
64
|
+
transitMiddleware → backendReducer → Redux Store
|
|
65
|
+
│
|
|
66
|
+
▼
|
|
67
|
+
Widget.connect() → React re-render
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Le backend sérialise les états Xcraft en transit JSON (via `xcraft-core-transport`) et les envoie au frontend. Le `transitMiddleware` désérialise et gère les générations pour détecter les pertes de paquets. Le `backendReducer` applique les patches ou remplace l'état complet.
|
|
71
|
+
|
|
72
|
+
### Flux de données frontend → backend
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
Widget React (interaction utilisateur)
|
|
76
|
+
│ cmd() / doFor()
|
|
77
|
+
▼
|
|
78
|
+
questMiddleware → send('QUEST', action)
|
|
79
|
+
│
|
|
80
|
+
▼
|
|
81
|
+
IPC Electron / WebSocket
|
|
82
|
+
│
|
|
83
|
+
▼
|
|
84
|
+
Bus Xcraft → quête Goblin
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Les actions utilisateur déclenchent des quêtes backend via le mécanisme `QUEST`. Le middleware sérialise l'action et l'envoie via IPC ou WebSocket selon le transport utilisé.
|
|
88
|
+
|
|
89
|
+
### Compensation optimiste
|
|
90
|
+
|
|
91
|
+
Le `backend-reducer` implémente un mécanisme de compensation : lors d'une modification de champ (`FIELD-CHANGED`), la valeur est mise à jour immédiatement dans le store frontend avant confirmation backend. Les compensateurs sont débounés à 300ms.
|
|
92
|
+
|
|
93
|
+
### Gestion des générations
|
|
94
|
+
|
|
95
|
+
Chaque état backend porte un numéro de génération. Si des générations sont perdues (réseau instable), le frontend demande un renvoi complet (`RESEND`). En mode patch (`_xcraftPatch`), seules les différences sont transmises.
|
|
96
|
+
|
|
97
|
+
## Exemples d'utilisation
|
|
98
|
+
|
|
99
|
+
### Créer un laboratoire (acteur Goblin)
|
|
100
|
+
|
|
101
|
+
```javascript
|
|
102
|
+
// Depuis un acteur Goblin
|
|
103
|
+
await quest.create('laboratory', {
|
|
104
|
+
id: 'laboratory@myapp',
|
|
105
|
+
desktopId: 'desktop@user1',
|
|
106
|
+
clientSessionId: 'client-session@abc',
|
|
107
|
+
url: 'http://localhost:3000',
|
|
108
|
+
config: {
|
|
109
|
+
feeds: ['workshop'],
|
|
110
|
+
themeContexts: ['theme'],
|
|
111
|
+
useWS: false,
|
|
112
|
+
title: 'Mon Application',
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Créer un carnotzet (mode WebSocket)
|
|
118
|
+
|
|
119
|
+
```javascript
|
|
120
|
+
await quest.create('carnotzet', {
|
|
121
|
+
id: 'carnotzet@session1',
|
|
122
|
+
clientSessionId: 'client-session@abc',
|
|
123
|
+
config: {
|
|
124
|
+
feed: 'desktop@user1',
|
|
125
|
+
feeds: ['workshop'],
|
|
126
|
+
theme: 'default',
|
|
127
|
+
themeContexts: ['theme'],
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Créer un widget connecté
|
|
133
|
+
|
|
134
|
+
```javascript
|
|
135
|
+
import Widget from 'goblin-laboratory/widgets/widget';
|
|
136
|
+
|
|
137
|
+
class MyWidget extends Widget {
|
|
138
|
+
render() {
|
|
139
|
+
const {name, age} = this.props;
|
|
140
|
+
return (
|
|
141
|
+
<div>
|
|
142
|
+
{name} ({age})
|
|
143
|
+
</div>
|
|
144
|
+
);
|
|
36
145
|
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export default Widget.connect((state, props) => ({
|
|
149
|
+
name: state.get(`backend.${props.id}.name`),
|
|
150
|
+
age: state.get(`backend.${props.id}.age`),
|
|
151
|
+
}))(MyWidget);
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Utiliser les props connectées avec `C` et `withC`
|
|
155
|
+
|
|
156
|
+
```javascript
|
|
157
|
+
import C from 'goblin-laboratory/widgets/connect-helpers/c';
|
|
158
|
+
import withC from 'goblin-laboratory/widgets/connect-helpers/with-c';
|
|
159
|
+
|
|
160
|
+
// Connecter un champ texte à l'état backend
|
|
161
|
+
const TextField = withC(TextFieldNC, {value: 'onChange'});
|
|
162
|
+
|
|
163
|
+
// Utilisation dans un render
|
|
164
|
+
<TextField value={C('.name')} />
|
|
165
|
+
|
|
166
|
+
// Avec transformation
|
|
167
|
+
<TextField value={C('.age', age => String(age), str => Number(str))} />
|
|
168
|
+
|
|
169
|
+
// Spread sur plusieurs props
|
|
170
|
+
<Label {...C('.person', ({firstname, lastname}) => ({
|
|
171
|
+
text: `${firstname} ${lastname}`,
|
|
172
|
+
}))} />
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Utiliser `WithModel` pour contextualiser les chemins
|
|
176
|
+
|
|
177
|
+
```javascript
|
|
178
|
+
import WithModel from 'goblin-laboratory/widgets/with-model/widget';
|
|
179
|
+
|
|
180
|
+
// Tous les C('.field') dans les enfants seront relatifs à backend.person@123
|
|
181
|
+
<WithModel model="backend.person@123">
|
|
182
|
+
<TextField value={C('.name')} />
|
|
183
|
+
<TextField value={C('.email')} />
|
|
184
|
+
</WithModel>;
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Utiliser l'acteur Termux depuis un autre Elf
|
|
188
|
+
|
|
189
|
+
```javascript
|
|
190
|
+
const termux = new Termux(this);
|
|
191
|
+
await termux.init();
|
|
192
|
+
// Le terminal est maintenant accessible via Alt+F12 dans l'UI
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Créer un Blueprint (métadonnée d'entité persistée)
|
|
196
|
+
|
|
197
|
+
```javascript
|
|
198
|
+
const blueprint = new Blueprint(this);
|
|
199
|
+
await blueprint.create('blueprint@case', desktopId);
|
|
200
|
+
await blueprint.change('fields.status.label', 'Nouveau statut');
|
|
37
201
|
```
|
|
38
202
|
|
|
39
|
-
|
|
203
|
+
## Interactions avec d'autres modules
|
|
204
|
+
|
|
205
|
+
- **[xcraft-core-goblin]** : Fournit les mécanismes de base Goblin (quêtes, Shredder, dispatch, Elf)
|
|
206
|
+
- **[xcraft-core-transport]** : Sérialisation/désérialisation des états en transit JSON
|
|
207
|
+
- **[xcraft-core-stones]** : Typage des shapes d'acteurs Elf (`string`, `boolean`, `array`, etc.)
|
|
208
|
+
- **[xcraft-core-utils]** : Utilitaires divers dont `parseOptions` pour le parsing de commandes
|
|
209
|
+
- **[xcraft-core-probe]** : Instrumentation des performances IPC/WebSocket
|
|
210
|
+
- **[xcraft-core-log]** : Journalisation des avertissements et erreurs internes
|
|
211
|
+
- **[goblin-theme]** : Fournit les builders de thème consommés par `ThemeContext`
|
|
212
|
+
|
|
213
|
+
## Configuration avancée
|
|
214
|
+
|
|
215
|
+
| Option | Description | Type | Valeur par défaut |
|
|
216
|
+
| ------------- | ------------------------------------------------- | -------- | ----------------- |
|
|
217
|
+
| `defaultZoom` | Facteur de zoom initial pour le frontend Electron | `number` | `1.0` |
|
|
218
|
+
|
|
219
|
+
### Variables d'environnement
|
|
220
|
+
|
|
221
|
+
| Variable | Description | Exemple | Valeur par défaut |
|
|
222
|
+
| ------------------ | -------------------------------------------------------- | ------------- | ----------------- |
|
|
223
|
+
| `GOBLINS_DEVTOOLS` | Active les DevTools Electron à l'ouverture de la fenêtre | `1` | non défini |
|
|
224
|
+
| `NODE_ENV` | Active Redux DevTools en mode développement | `development` | non défini |
|
|
225
|
+
|
|
226
|
+
## Détails des sources
|
|
227
|
+
|
|
228
|
+
### `lib/carnotzet.js`
|
|
229
|
+
|
|
230
|
+
Acteur Goblin léger destiné aux interfaces sans fenêtre native (mode WebSocket/navigateur). Il joue le même rôle de coordinateur que `laboratory` mais sans gestion de fenêtre Electron.
|
|
231
|
+
|
|
232
|
+
#### Cycle de vie
|
|
233
|
+
|
|
234
|
+
À la création (`create`), le carnotzet :
|
|
235
|
+
|
|
236
|
+
1. Valide que `config.feed` est présent
|
|
237
|
+
2. Crée un acteur `theme-composer` pour chaque contexte de thème
|
|
238
|
+
3. Souscrit au warehouse pour les feeds configurés
|
|
239
|
+
4. S'abonne à `goblin.released` pour nettoyer les widgets libérés
|
|
240
|
+
|
|
241
|
+
À la destruction (`delete`), il se désabonne du warehouse et libère sa branche.
|
|
242
|
+
|
|
243
|
+
#### Méthodes publiques (quêtes)
|
|
244
|
+
|
|
245
|
+
- **`create(clientSessionId, config)`** — Initialise le carnotzet avec la configuration du feed et des thèmes.
|
|
246
|
+
- **`get-feed()`** — Retourne l'identifiant du feed courant.
|
|
247
|
+
- **`set-root(widget, widgetId)`** — Définit le widget racine à afficher.
|
|
248
|
+
- **`del(widgetId)`** — Supprime un widget du feed (désabonnement warehouse).
|
|
249
|
+
- **`change-theme(name)`** — Change le thème actif.
|
|
250
|
+
- **`when-ui-crash(desktopId, error, info)`** — Journalise les erreurs de rendu React.
|
|
251
|
+
|
|
252
|
+
### `widgets/laboratory/service.js`
|
|
253
|
+
|
|
254
|
+
Acteur Goblin principal du module. Il orchestre la création complète d'une fenêtre applicative Electron, y compris la gestion du zoom, du thème, des feeds warehouse, et du terminal Termux.
|
|
255
|
+
|
|
256
|
+
#### Cycle de vie
|
|
257
|
+
|
|
258
|
+
1. **`create`** : instancie le Termux, crée les `theme-composer`, crée le `wm`, initialise zoom et thème depuis la session client, abonne les listeners de fermeture de fenêtre et de reload de thème.
|
|
259
|
+
2. **`listen`** : active les abonnements aux événements de navigation, changement de thème et dispatch provenant du desktop.
|
|
260
|
+
3. **`close`** : ferme la fenêtre via `client-session` et `client`, puis libère l'acteur.
|
|
261
|
+
4. **`delete`** : désactive les listeners (`unlisten`).
|
|
262
|
+
|
|
263
|
+
#### Méthodes publiques (quêtes)
|
|
264
|
+
|
|
265
|
+
- **`create(desktopId, clientSessionId, userId, url, config)`** — Crée la fenêtre et l'infrastructure complète.
|
|
266
|
+
- **`close()`** — Ferme proprement la fenêtre et libère l'acteur.
|
|
267
|
+
- **`listen(desktopId, userId, useConfigurator)`** — Active l'écoute des événements du desktop.
|
|
268
|
+
- **`set-root(widget, widgetId, themeContext)`** — Définit le widget racine affiché.
|
|
269
|
+
- **`set-feed(desktopId)`** — Change le feed souscrit (greffe les branches dans le warehouse).
|
|
270
|
+
- **`change-theme(name)`** — Change le thème et persiste la préférence.
|
|
271
|
+
- **`zoom()` / `un-zoom()` / `default-zoom()` / `change-zoom(zoom)`** — Contrôle du zoom avec persistance.
|
|
272
|
+
- **`dispatch(action)`** — Envoie une action Redux directement au frontend.
|
|
273
|
+
- **`nav(route)`** — Navigation vers une route.
|
|
274
|
+
- **`duplicate(forId)`** — Duplique le laboratoire dans une nouvelle fenêtre.
|
|
275
|
+
- **`del(widgetId)`** — Supprime un widget du feed ou ferme l'application selon le contexte.
|
|
276
|
+
|
|
277
|
+
### `lib/termux.js` et `termux.js`
|
|
278
|
+
|
|
279
|
+
#### Rôle
|
|
280
|
+
|
|
281
|
+
Le Termux est un terminal de commandes intégré à l'interface, accessible via `Alt+F12`. Il permet d'exécuter des commandes du bus Xcraft directement depuis l'UI, avec autocomplétion, historique de navigation, et gestion des signaux (`SIGINT`).
|
|
282
|
+
|
|
283
|
+
L'acteur `Termux` est un `Elf.Alone` (singleton). La logique de mutation d'état est dans `TermuxLogic` (un `Elf.Spirit`).
|
|
284
|
+
|
|
285
|
+
#### État et modèle de données
|
|
286
|
+
|
|
287
|
+
L'état est décrit par `TermuxShape` :
|
|
288
|
+
|
|
289
|
+
| Champ | Type | Description |
|
|
290
|
+
| -------------- | ---------------- | ------------------------------------------------------ |
|
|
291
|
+
| `id` | `string` | Identifiant de l'acteur (`"termux"`) |
|
|
292
|
+
| `prompt` | `string` | Invite de commande courante (`"~ $"` ou `"~ #"`) |
|
|
293
|
+
| `busy` | `boolean` | Indique si une commande est en cours d'exécution |
|
|
294
|
+
| `history` | `array(string)` | Historique des entrées et sorties affichées |
|
|
295
|
+
| `completion` | `string` | Suggestion d'autocomplétion courante |
|
|
296
|
+
| `value` | `string` | Valeur courante de la ligne de saisie |
|
|
297
|
+
| `toolName` | `option(string)` | Nom de l'outil en cours d'exécution (pour SIGINT) |
|
|
298
|
+
| `inputCommand` | `boolean` | Indique si le terminal attend une saisie interactive |
|
|
299
|
+
| `cmd` | `option(string)` | Commande en attente d'input interactif |
|
|
300
|
+
| `args` | `option(object)` | Arguments de la commande en attente d'input interactif |
|
|
301
|
+
|
|
302
|
+
#### Cycle de vie
|
|
303
|
+
|
|
304
|
+
- **`init()`** — Initialise le prompt selon le rang de l'utilisateur (`admin` → `~ #`, sinon `~ $`), charge les outils disponibles depuis le registre de commandes, s'abonne aux événements `<termux-input>` et `<termux-output>`, écoute les changements de registre de commandes.
|
|
305
|
+
- **`dispose()`** — Désabonne le listener du registre de commandes.
|
|
306
|
+
|
|
307
|
+
#### Méthodes publiques
|
|
308
|
+
|
|
309
|
+
- **`init()`** — Initialise le terminal (idempotent).
|
|
310
|
+
- **`beginCommand(command)`** — Parse et exécute une commande, met à jour l'historique.
|
|
311
|
+
- **`endCommand(result)`** — Termine une commande et affiche le résultat.
|
|
312
|
+
- **`inputCommand(input)`** — Traite une saisie interactive (mode `forInputCommand`).
|
|
313
|
+
- **`forInputCommand(question, cmd, args)`** — Met le terminal en attente d'une saisie utilisateur.
|
|
314
|
+
- **`forOutputCommand(value)`** — Ajoute une sortie dans l'historique.
|
|
315
|
+
- **`askForCompletion(input)`** — Calcule et affiche les suggestions d'autocomplétion.
|
|
316
|
+
- **`setFromHistory(up, input)`** — Navigation dans l'historique des commandes (haut/bas).
|
|
317
|
+
- **`clearCompletion()`** — Efface la suggestion courante.
|
|
318
|
+
- **`signal(signal)`** — Envoie un signal (`SIGINT`) à la commande en cours.
|
|
319
|
+
|
|
320
|
+
#### Outils intégrés (quêtes `$tool`)
|
|
321
|
+
|
|
322
|
+
Les outils sont des commandes spéciales enregistrées avec le suffixe `$tool`. Ils sont listés et exécutables directement depuis le terminal Termux.
|
|
323
|
+
|
|
324
|
+
- **`clear$tool()`** — Vide l'historique du terminal.
|
|
325
|
+
- **`man$tool(name)`** — Affiche la documentation d'une commande (module, emplacement, usage, paramètres).
|
|
326
|
+
- **`buslog$tool(horde, verbosityLevel, ...moduleNames)`** — Configure le niveau de verbosité des logs du bus (admin uniquement pour les hordes passives).
|
|
327
|
+
- **`metrics$tool(horde, output?)`** — Récupère les métriques du bus Xcraft au format JSON, avec export optionnel vers un fichier.
|
|
328
|
+
- **`heapdump$tool(horde)`** — Déclenche un heap dump sur le processus cible.
|
|
329
|
+
- **`malloctrim$tool(horde)`** — Libère la mémoire non utilisée via `malloc_trim`.
|
|
330
|
+
|
|
331
|
+
### `lib/index.js`
|
|
332
|
+
|
|
333
|
+
Fournit les classes `ElectronChannel` et `WebSocketChannel` utilisées par le backend pour envoyer des messages au frontend.
|
|
334
|
+
|
|
335
|
+
- **`ElectronChannel(win)`** — Utilise `win.webContents.send` pour la communication intra-processus Electron.
|
|
336
|
+
- **`WebSocketChannel(win)`** — Utilise une connexion WebSocket pour les clients distants ou les navigateurs.
|
|
337
|
+
|
|
338
|
+
Les deux canaux exposent : `sendBackendState(msg)`, `sendPushPath(path)`, `sendAction(action)`, `beginRender(labId, tokens)`.
|
|
339
|
+
|
|
340
|
+
### `lib/blueprints/blueprint.js`
|
|
341
|
+
|
|
342
|
+
Acteur Elf persisté (`Elf.Archetype`) représentant un blueprint d'entité. Les blueprints décrivent la structure d'une entité (champs, références, collections, configuration UI) sous forme de métadonnées persistées dans la base `blueprints`.
|
|
343
|
+
|
|
344
|
+
#### État et modèle de données
|
|
345
|
+
|
|
346
|
+
L'état est décrit par `BlueprintShape` :
|
|
347
|
+
|
|
348
|
+
| Champ | Type | Description |
|
|
349
|
+
| ------------- | ----------------------------------------- | -------------------------------------- |
|
|
350
|
+
| `id` | `string` | Identifiant du blueprint |
|
|
351
|
+
| `entity` | `string` | Nom de l'entité décrite (ex: `"case"`) |
|
|
352
|
+
| `fields` | `record(string, FieldShape)` | Dictionnaire des champs de l'entité |
|
|
353
|
+
| `references` | `option(record(string, ReferenceShape))` | Pointeurs vers d'autres entités |
|
|
354
|
+
| `collections` | `option(record(string, CollectionShape))` | Collections de pointeurs |
|
|
355
|
+
| `ui` | `option(UiConfigShape)` | Configuration UI globale de l'entité |
|
|
356
|
+
|
|
357
|
+
Les shapes imbriquées clés :
|
|
358
|
+
|
|
359
|
+
- **`FieldShape`** : `type` (text, enum, date…), `label`, `required`, `readonly`, `hidden`, `values` (pour les enums), `ui` (hints de filtrage, tri, recherche)
|
|
360
|
+
- **`ReferenceShape`** : `entity` (cible), `label`, `lookup` (représentation dans l'UI avec `labelPaths`, `iconPath`, `drilldown`)
|
|
361
|
+
- **`UiConfigShape`** : `icon`, `primaryLabel`, `secondaryLabel`, `defaultSort`
|
|
362
|
+
|
|
363
|
+
#### Méthodes publiques
|
|
364
|
+
|
|
365
|
+
- **`create(id, desktopId)`** — Crée et persiste le blueprint.
|
|
366
|
+
- **`change(path, newValue)`** — Met à jour un champ du blueprint et persiste.
|
|
367
|
+
- **`delete()`** — Destructeur (no-op actuellement).
|
|
368
|
+
|
|
369
|
+
### `lib/blueprints/blueprints.js`
|
|
370
|
+
|
|
371
|
+
Acteur Elf singleton (`Elf.Alone`) qui charge l'ensemble des blueprints persistés au démarrage.
|
|
372
|
+
|
|
373
|
+
#### Méthodes publiques
|
|
374
|
+
|
|
375
|
+
- **`loadAll(desktopId)`** — Lit tous les IDs depuis la base `blueprints` via `cryo.reader` et monte chaque `Blueprint` dans la session desktop.
|
|
376
|
+
|
|
377
|
+
### `widgets/widget/index.js`
|
|
378
|
+
|
|
379
|
+
La classe `Widget` est la brique fondamentale de tous les composants graphiques Xcraft. Elle étend `React.Component` et fournit :
|
|
380
|
+
|
|
381
|
+
**Connexion au store :**
|
|
382
|
+
|
|
383
|
+
- **`Widget.connect(mapStateToProps)`** — HOC de connexion Redux avec égalité Shredder optimisée.
|
|
384
|
+
- **`Widget.connectBackend(mapStateToProps)`** — Connexion automatique sur `backend.${props.id}`, affiche `null` si l'état n'est pas chargé.
|
|
385
|
+
- **`Widget.connectWidget(mapStateToProps)`** — Connexion sur `widgets.${props.id}`.
|
|
386
|
+
- **`Widget.Wired(Component)`** — Connecte automatiquement selon la définition `static get wiring()`.
|
|
387
|
+
|
|
388
|
+
**Communication avec le backend :**
|
|
389
|
+
|
|
390
|
+
- **`cmd(cmd, args)`** — Envoie une quête au bus Xcraft (vérifie les droits via le registre).
|
|
391
|
+
- **`do(action, args)`** — Appelle une quête sur le service correspondant au nom du widget.
|
|
392
|
+
- **`doFor(serviceId, action, args)`** — Appelle une quête sur un service spécifique par ID.
|
|
393
|
+
- **`doDispatch(model, name, args)`** — Route vers `doFor` (backend) ou `dispatchTo` (widgets) selon le modèle.
|
|
394
|
+
- **`canDo(cmd)`** — Vérifie si une commande est autorisée pour l'utilisateur courant.
|
|
395
|
+
|
|
396
|
+
**Dispatch Redux :**
|
|
397
|
+
|
|
398
|
+
- **`dispatch(action, name?)`** — Dispatch dans le reducer frontend du widget courant.
|
|
399
|
+
- **`dispatchTo(id, action, name?)`** — Dispatch dans le reducer d'un widget cible.
|
|
400
|
+
- **`dispatchToCache(id, payload)`** — Stocke une valeur dans le cache du desktop (persisté entre montages).
|
|
401
|
+
- **`rawDispatch(action)`** — Dispatch direct dans le store Redux.
|
|
402
|
+
|
|
403
|
+
**Accès à l'état :**
|
|
404
|
+
|
|
405
|
+
- **`getState(path?)`** — Retourne l'état complet du store ou une valeur à un chemin.
|
|
406
|
+
- **`getBackendState(path?)`** — Retourne l'état backend de ce widget ou d'un ID spécifié.
|
|
407
|
+
- **`getWidgetState(path?)`** — Retourne l'état frontend du widget.
|
|
408
|
+
- **`getWidgetCacheState(widgetId)`** — Retourne la valeur du cache desktop pour un widget.
|
|
409
|
+
|
|
410
|
+
**Styles :**
|
|
411
|
+
La propriété `styles` est calculée via Aphrodite à partir d'un fichier `styles.js` companion. Le système fusionne les définitions de styles héritées et met en cache les résultats (LRU 2048 entrées).
|
|
412
|
+
|
|
413
|
+
**Navigation :**
|
|
414
|
+
|
|
415
|
+
- **`nav(route, frontOnly?)`** — Navigation via le router (frontend seul ou via le laboratoire).
|
|
416
|
+
|
|
417
|
+
**Utilitaires :**
|
|
418
|
+
|
|
419
|
+
- **`setBackendValue(path, value)`** — Modifie directement une valeur dans le state backend (compensation).
|
|
420
|
+
- **`reportError(error, info)`** — Remonte une erreur React au laboratoire.
|
|
421
|
+
- **`Widget.copyTextToClipboard(text)`** — Copie du texte dans le presse-papiers.
|
|
422
|
+
- **`Widget.getUserSession(state)`** — Retourne la session utilisateur courante depuis le state.
|
|
423
|
+
- **`Widget.getLoginSession(state)`** — Retourne la session de login courante.
|
|
424
|
+
- **`Widget.getSchema(state, path?)`** — Retourne le schéma depuis `workshop.schema`.
|
|
425
|
+
|
|
426
|
+
### `widgets/renderer.js`, `widgets/index-electron.js`, `widgets/index-browsers.js`, `widgets/index-electron-ws.js`
|
|
427
|
+
|
|
428
|
+
Ces fichiers constituent le bootstrap du frontend React selon le mode de rendu :
|
|
429
|
+
|
|
430
|
+
- **`ElectronRenderer`** (`index-electron.js`) — Utilise `ipcRenderer` pour communiquer avec le main process Electron. Récupère `wid` et `labId` depuis les paramètres d'URL.
|
|
431
|
+
- **`BrowsersRenderer`** (`index-browsers.js`) — Utilise WebSocket avec reconnexion exponentielle (125ms → doublement), gestion des tokens de session via `localStorage`/`sessionStorage`. Supporte le cas `Epsitec.Cresus.Shell` avec token via cookie.
|
|
432
|
+
- **`ElectronRendererWS`** (`index-electron-ws.js`) — Variante Electron utilisant WebSocket (pour les fenêtres secondaires ou le mode hybride). Récupère le port WebSocket via le paramètre `wss=` dans l'URL.
|
|
433
|
+
|
|
434
|
+
Tous héritent de `Renderer` (`renderer.js`) qui initialise le store Redux, l'historique de navigation et les handlers de drag & drop. Les messages JSON entrants sont parsés via un Web Worker dédié pour éviter de bloquer le thread principal.
|
|
435
|
+
|
|
436
|
+
### `widgets/store/`
|
|
437
|
+
|
|
438
|
+
Le store Redux est composé de plusieurs reducers combinés :
|
|
439
|
+
|
|
440
|
+
- **`backend-reducer`** — Gère l'état backend reçu du serveur. Supporte les patches différentiels (`_xcraftPatch`), les compensations optimistes et la mise à jour directe de champs (`FIELD-CHANGED`).
|
|
441
|
+
- **`widgets-reducer`** — Gère les états locaux frontend des widgets (découverte dynamique des reducers par namespace). Supporte `WIDGETS_COLLECT` pour nettoyer les états orphelins.
|
|
442
|
+
- **`commands-reducer`** — Maintient le registre des commandes disponibles sur le bus (`COMMANDS_REGISTRY`).
|
|
443
|
+
- **`network-reducer`** — Suit l'état de connexion des hordes (lag, overlay, message de déconnexion) via `CONNECTION_STATUS`.
|
|
444
|
+
- **`app-reducer`** — Délègue aux reducers d'application spécifiques (`app-reducer` de chaque module goblin, discriminé par `action._appName`).
|
|
445
|
+
- **`router-reducer`** — Gère la navigation (compatible `connected-react-router`).
|
|
446
|
+
|
|
447
|
+
Les middlewares configurés :
|
|
448
|
+
|
|
449
|
+
- **`transitMiddleware`** — Désérialise les états de transit, gère les générations, déclenche le resend en cas de perte, injecte les compensateurs.
|
|
450
|
+
- **`questMiddleware`** — Sérialise et envoie les actions `QUEST` au backend via le canal de communication.
|
|
451
|
+
- **`formMiddleware`** — Intercepte `FIELD-CHANGED` et les actions de formulaire (`rrf/change`) pour déclencher automatiquement les quêtes de mise à jour backend (`{goblin}.change` ou `{goblin}.change-{field}`) avec debounce 200ms pour les hinters.
|
|
452
|
+
|
|
453
|
+
### `widgets/connect-helpers/`
|
|
454
|
+
|
|
455
|
+
Ensemble d'utilitaires pour la connexion déclarative des props :
|
|
456
|
+
|
|
457
|
+
- **`C(path, inFunc?, outFunc?)`** — Crée une `ConnectedProp` liant une prop à un chemin dans le state. Supporte les tableaux de chemins pour passer plusieurs valeurs à `inFunc`.
|
|
458
|
+
- **`withC(Component, dispatchProps?, options?)`** — HOC qui donne à un composant la capacité de recevoir des `ConnectedProp`. Gère les chemins relatifs/absolus (via `ModelContext`), les transformations et les dispatches retour. L'option `modelProp` permet de définir le contexte de modèle à partir d'une prop connectée.
|
|
459
|
+
- **`joinModels(baseModel, nextModel)`** — Résout les chemins relatifs (préfixés par `.`) par rapport à un modèle de base.
|
|
460
|
+
|
|
461
|
+
### `widgets/theme-context/widget.js`
|
|
462
|
+
|
|
463
|
+
Composant `ThemeContext` qui injecte le thème calculé dynamiquement dans l'arbre React. Il :
|
|
464
|
+
|
|
465
|
+
1. Importe le contexte de thème (builders) via l'importer
|
|
466
|
+
2. Appelle les builders (`paletteBuilder`, `shapesBuilder`, `stylesBuilder`, etc.) pour construire le thème complet
|
|
467
|
+
3. Injecte les styles globaux CSS et les polices via des balises `<style>`
|
|
468
|
+
4. Force le re-rendu de tous les enfants via la prop `key` (basée sur `cacheName`) lors d'un changement de thème
|
|
469
|
+
|
|
470
|
+
Le `cacheName` combine le nom du thème et sa génération (incrémentée à chaque `reload-theme`) pour invalider le cache Aphrodite.
|
|
471
|
+
|
|
472
|
+
### `widgets/termux/widget.js`
|
|
473
|
+
|
|
474
|
+
Widget React connecté qui affiche le terminal Termux. Activé par `Alt+F12`, il s'affiche en superposition semi-transparente sur l'interface. Il gère :
|
|
475
|
+
|
|
476
|
+
- La saisie clavier avec raccourcis bash (`Ctrl+A`/`Ctrl+E` curseur, `Ctrl+U` effacer ligne, `Ctrl+W` effacer mot, `Tab` autocomplétion, flèches historique)
|
|
477
|
+
- L'historique défilant avec rendu inversé (le plus récent en bas)
|
|
478
|
+
- La transmission des signaux (`Ctrl+C` → `SIGINT`)
|
|
479
|
+
- L'autocomplétion via Tab
|
|
480
|
+
|
|
481
|
+
### `widgets/disconnect-overlay/widget.js`
|
|
482
|
+
|
|
483
|
+
Overlay plein écran affiché lorsque la connexion au backend est perdue. Affiche une icône réseau clignotante (animation 1.2s) et un message d'état. Le fond noir semi-transparent avec `backdrop-filter: blur` est positionné en `fixed` avec `z-index` configurable (défaut 20).
|
|
484
|
+
|
|
485
|
+
**Props :** `message` (string), `zIndex` (number, défaut 20), `children` (le contenu rendu derrière l'overlay).
|
|
486
|
+
|
|
487
|
+
### `widgets/maintenance/widget.js`
|
|
488
|
+
|
|
489
|
+
Overlay plein écran affiché pendant une opération de maintenance. Affiche une icône de verrouillage, une barre de progression et un message. Utilise le wiring automatique (`Widget.Wired`) pour lire `maintenance.status`, `maintenance.progress` et `maintenance.message`.
|
|
490
|
+
|
|
491
|
+
**Wiring :** `{id: 'id', status: 'maintenance.status', progress: 'maintenance.progress', message: 'maintenance.message'}`.
|
|
492
|
+
|
|
493
|
+
### `widgets/error-handler/widget.js`
|
|
494
|
+
|
|
495
|
+
Error boundary React (`getDerivedStateFromError`) qui capture les erreurs de rendu des composants enfants. Affiche une icône d'avertissement orange cliquable pour relancer le rendu (`setState({error: null})`).
|
|
496
|
+
|
|
497
|
+
**Props :** `big` (boolean, agrandit l'icône à 400%), `renderError` (function, personnalise le rendu d'erreur), `children`.
|
|
498
|
+
|
|
499
|
+
### `widgets/state-loader/widget.js`
|
|
500
|
+
|
|
501
|
+
Composant utilitaire qui attend qu'un état backend soit chargé avant de rendre ses enfants. Accepte `path` (relatif à `backend.`, vérifie l'existence de `.id`) ou `fullPath` (chemin absolu). Affiche optionnellement un `FallbackComponent` pendant le chargement.
|
|
502
|
+
|
|
503
|
+
### `widgets/collection-loader/widget.js`
|
|
504
|
+
|
|
505
|
+
Variante de `StateLoader` pour les collections : attend que tous les IDs d'une liste (`props.ids`) soient présents dans le backend avant de rendre les enfants. Si les enfants sont une fonction, elle leur passe la collection mappée comme argument.
|
|
506
|
+
|
|
507
|
+
### `widgets/with-model/widget.js`
|
|
508
|
+
|
|
509
|
+
Composant fournisseur de contexte de modèle. Définit `context.model` pour tous les enfants, permettant aux props `C('.relativePath')` de se résoudre correctement. Compatible avec l'API Context React moderne (`ModelContext`) et l'ancienne API legacy (`childContextTypes`).
|
|
510
|
+
|
|
511
|
+
### `widgets/with-desktop-id/widget.js` et `widgets/with-readonly/widget.js`
|
|
512
|
+
|
|
513
|
+
Fournisseurs de contexte pour `desktopId` et `readonly`, accessibles via hooks (`useDesktopId()`, `useReadonly()`) ou via le contexte legacy (`childContextTypes`). Exposent respectivement `DesktopIdContext` et `ReadonlyContext`.
|
|
514
|
+
|
|
515
|
+
### `widgets/with-workitem/widget.js`
|
|
40
516
|
|
|
41
|
-
|
|
517
|
+
Fournisseur de contexte qui expose `id`, `entityId` et `dragServiceId` aux descendants, tout en wrappant les enfants dans `WithReadonly`.
|
|
42
518
|
|
|
43
|
-
|
|
519
|
+
### `widgets/with-route/with-route.js`
|
|
44
520
|
|
|
45
|
-
|
|
46
|
-
Certaine propriétés du widget ne sont pas cablée sur le state est peuvent être définie au rendu.
|
|
521
|
+
HOC de connexion au router Redux. Permet de connecter un composant à la route courante avec surveillance de paramètres (`watchedParams`), query strings (`watchedSearchs`) et hash (`watchHash`). Expose `isDisplayed` (booléen) indiquant si la route courante correspond. Optimisé avec `shallowEqualShredder`.
|
|
47
522
|
|
|
48
|
-
|
|
523
|
+
### `widgets/frame/widget.js`
|
|
49
524
|
|
|
50
|
-
|
|
525
|
+
Composant racine qui fournit le store Redux, le `labId`, `dispatch` et le thème aux composants enfants via le contexte React legacy. Utilisé pour encapsuler des sous-arbres React dans un contexte Xcraft complet (ex : fenêtres secondaires).
|
|
51
526
|
|
|
52
|
-
|
|
527
|
+
### `widgets/root/index.js`
|
|
53
528
|
|
|
54
|
-
|
|
529
|
+
Composant racine monté par `Renderer`. Fournit le store Redux via `<Provider>` et optionnellement le router via `<ConnectedRouter>`. Instancie le widget `Laboratory` correspondant au `labId`, avec ou sans routing selon `props.useRouter`.
|
|
55
530
|
|
|
56
|
-
|
|
531
|
+
### `widgets/importer/`
|
|
57
532
|
|
|
58
|
-
|
|
533
|
+
Système de découverte dynamique des widgets via `require.context` Webpack. Permet d'importer n'importe quel type de ressource widget (`widget`, `styles`, `reducer`, `theme-context`, `app-reducer`, `compensator`, etc.) par namespace. Un importer personnalisé peut être fourni par `mainGoblinModule` (via `lib/.webpack-config.js`).
|
|
59
534
|
|
|
60
|
-
|
|
61
|
-
- pilotage du goblin wm
|
|
62
|
-
- persistance des paramètres liés a l'espace de travail inter-gadgets
|
|
63
|
-
- composant d'auto-layout
|
|
64
|
-
- persistance de settings de fenêtre inter-gadgets (écoute events wm)
|
|
65
|
-
- lazy loading des gagdgets https://webpack.js.org/guides/lazy-load-react/
|
|
535
|
+
### `lib/.webpack-config.js`
|
|
66
536
|
|
|
67
|
-
|
|
537
|
+
Génère la configuration des alias Webpack pour le bundle frontend. Résout les aliases pour `t` (traductions Nabu), `nabu`, `goblin_importer` (avec support d'un importer personnalisé depuis `mainGoblinModule`) et `goblin_theme_fa` (FontAwesome Pro ou Free selon disponibilité du package `@fortawesome/fontawesome-pro`).
|
|
68
538
|
|
|
69
|
-
|
|
539
|
+
### `lib/helpers.js`
|
|
70
540
|
|
|
71
|
-
|
|
541
|
+
Utilitaire de parsing d'URL : `getParameter(search, name)` extrait un paramètre d'une query string (décode les caractères URL-encodés).
|
|
72
542
|
|
|
73
|
-
`
|
|
543
|
+
### `widgets/frontend-form/`
|
|
74
544
|
|
|
75
|
-
|
|
545
|
+
Composant `FrontendForm` qui crée un contexte `WithModel` sur `widgets.${widgetId}`. Le `reducer.js` associé gère les actions `INIT` (initialisation de l'état si vide) et `CHANGE` (mise à jour d'un chemin) pour un état de formulaire purement frontend.
|
|
76
546
|
|
|
77
|
-
`
|
|
547
|
+
### `widgets/store/middlewares.js`
|
|
78
548
|
|
|
79
|
-
|
|
549
|
+
Détails des middlewares Redux :
|
|
80
550
|
|
|
81
|
-
`
|
|
551
|
+
**`questMiddleware`** — Intercepte les actions de type `QUEST` et les envoie au backend via le canal de communication. Gère les states de compensation.
|
|
82
552
|
|
|
83
|
-
|
|
84
|
-
dans la même fenetre:
|
|
553
|
+
**`formMiddleware`** — Intercepte `FIELD-CHANGED` et les actions de formulaire (`rrf/change`, `rrf/batch`, `hinter/search`) pour déclencher automatiquement les quêtes de mise à jour backend correspondantes (`{goblin}.change` ou `{goblin}.change-{field}`). Utilise un debounce de 200ms pour les recherches hinter.
|
|
85
554
|
|
|
86
|
-
`
|
|
555
|
+
**`transitMiddleware`** — Désérialise les états backend (`NEW_BACKEND_STATE`), vérifie la continuité des générations et injecte les compensateurs. Déclenche `RESEND` si des générations sont perdues.
|
|
87
556
|
|
|
88
|
-
|
|
557
|
+
### `widgets/widget/style/`
|
|
89
558
|
|
|
90
|
-
|
|
559
|
+
Pipeline de style Aphrodite optimisé :
|
|
91
560
|
|
|
92
|
-
|
|
561
|
+
- `build-style.js` — Point d'entrée, orchestre le calcul et met en cache via `StylesCache`. Étend Aphrodite avec un handler de sélecteur personnalisé supportant le sélecteur `&` (pour les styles imbriqués).
|
|
562
|
+
- `styles-cache.js` — Cache LRU (2048 entrées max) avec liste doublement chaînée pour l'éviction. Les entrées les moins récemment utilisées migrent vers le début de la liste.
|
|
563
|
+
- `compute-style-hash.js` — Hash basé sur `theme.cacheName` (si le style dépend du thème) et la sérialisation stable des props de style (via `safe-stable-stringify`).
|
|
564
|
+
- `get-style-props.js` — Extrait uniquement les props déclarées dans `propNames` (et applique `mapProps` si défini) pour le calcul de style.
|
|
565
|
+
- `merge-style-definitions.js` — Fusionne les définitions de style de la hiérarchie d'héritage du widget en une seule définition combinée.
|
|
93
566
|
|
|
94
|
-
`
|
|
567
|
+
### `widgets/searchkit/index.js`
|
|
95
568
|
|
|
96
|
-
|
|
569
|
+
Intégration expérimentale avec Elasticsearch via SearchKit. Permet d'afficher un champ de recherche full-text et les résultats depuis un index Elasticsearch local (`http://localhost:9200`). Ce composant est considéré comme expérimental.
|
|
97
570
|
|
|
98
|
-
|
|
571
|
+
## Licence
|
|
99
572
|
|
|
100
|
-
|
|
573
|
+
Ce module est distribué sous [licence MIT](./LICENSE).
|
|
101
574
|
|
|
102
|
-
|
|
575
|
+
---
|
|
103
576
|
|
|
104
|
-
|
|
577
|
+
[xcraft-core-goblin]: https://github.com/Xcraft-Inc/xcraft-core-goblin
|
|
578
|
+
[xcraft-core-transport]: https://github.com/Xcraft-Inc/xcraft-core-transport
|
|
579
|
+
[xcraft-core-stones]: https://github.com/Xcraft-Inc/xcraft-core-stones
|
|
580
|
+
[xcraft-core-utils]: https://github.com/Xcraft-Inc/xcraft-core-utils
|
|
581
|
+
[xcraft-core-probe]: https://github.com/Xcraft-Inc/xcraft-core-probe
|
|
582
|
+
[xcraft-core-log]: https://github.com/Xcraft-Inc/xcraft-core-log
|
|
583
|
+
[goblin-theme]: https://github.com/Xcraft-Inc/goblin-theme
|
|
105
584
|
|
|
106
|
-
|
|
107
|
-
`laboratory.show /syncui`
|
|
585
|
+
_Ce contenu a été généré par IA_
|
package/config.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "goblin-laboratory",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.11.1",
|
|
4
4
|
"description": "Laboratory",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"prop-types": "^15.5.10",
|
|
49
49
|
"react": "^17.0.1",
|
|
50
50
|
"react-dom": "^17.0.1",
|
|
51
|
-
"react-redux": "^
|
|
51
|
+
"react-redux": "^8.1.3",
|
|
52
52
|
"react-router": "^5.0.0",
|
|
53
53
|
"redux-thunk": "^2.3.0",
|
|
54
54
|
"safe-stable-stringify": "^2.5.0",
|
|
@@ -58,4 +58,4 @@
|
|
|
58
58
|
"xcraft-traverse": "^0.7.0"
|
|
59
59
|
},
|
|
60
60
|
"prettier": "xcraft-dev-prettier"
|
|
61
|
-
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import C, {ConnectedProp} from './c.js';
|
|
2
|
+
|
|
3
|
+
export default function mapC(value, inFunc, outFunc) {
|
|
4
|
+
if (value instanceof ConnectedProp) {
|
|
5
|
+
return C(
|
|
6
|
+
value.path,
|
|
7
|
+
value.inFunc ? (...args) => inFunc(value.inFunc(...args)) : inFunc,
|
|
8
|
+
value.outFunc
|
|
9
|
+
? value.outFunc.length > 1 || outFunc.length > 1
|
|
10
|
+
? (newValue, ...oldValues) =>
|
|
11
|
+
value.outFunc(outFunc(newValue, ...oldValues), ...oldValues)
|
|
12
|
+
: (newValue) => value.outFunc(outFunc(newValue))
|
|
13
|
+
: outFunc
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
return inFunc(value);
|
|
17
|
+
}
|
|
@@ -225,8 +225,21 @@ export default function withC(Component, dispatchProps = {}, {modelProp} = {}) {
|
|
|
225
225
|
const dispatchPropName = dispatchProps[name];
|
|
226
226
|
const outFunc = prop.outFunc;
|
|
227
227
|
if (outFunc) {
|
|
228
|
-
|
|
229
|
-
|
|
228
|
+
if (outFunc.length > 1) {
|
|
229
|
+
let currentValues;
|
|
230
|
+
if (Array.isArray(prop.fullPath)) {
|
|
231
|
+
currentValues = prop.fullPath.map((path) =>
|
|
232
|
+
this.getState(path)
|
|
233
|
+
);
|
|
234
|
+
} else {
|
|
235
|
+
currentValues = [this.getState(prop.fullPath)];
|
|
236
|
+
}
|
|
237
|
+
onChangeProps[dispatchPropName] = (value) =>
|
|
238
|
+
this.handlePropChange(name, outFunc(value, ...currentValues));
|
|
239
|
+
} else {
|
|
240
|
+
onChangeProps[dispatchPropName] = (value) =>
|
|
241
|
+
this.handlePropChange(name, outFunc(value));
|
|
242
|
+
}
|
|
230
243
|
} else {
|
|
231
244
|
onChangeProps[dispatchPropName] = (value) =>
|
|
232
245
|
this.handlePropChange(name, value);
|
|
@@ -85,14 +85,13 @@ const logicHandlers = {
|
|
|
85
85
|
};
|
|
86
86
|
|
|
87
87
|
// Register quest's according rc.json
|
|
88
|
-
Goblin.registerQuest(goblinName, 'create', function
|
|
88
|
+
Goblin.registerQuest(goblinName, 'create', async function (
|
|
89
89
|
quest,
|
|
90
90
|
desktopId,
|
|
91
91
|
clientSessionId,
|
|
92
92
|
userId,
|
|
93
93
|
url,
|
|
94
|
-
config
|
|
95
|
-
next
|
|
94
|
+
config
|
|
96
95
|
) {
|
|
97
96
|
quest.goblin.setX('url', url);
|
|
98
97
|
quest.goblin.setX('desktopId', desktopId);
|
|
@@ -106,7 +105,7 @@ Goblin.registerQuest(goblinName, 'create', function* (
|
|
|
106
105
|
config.feeds.push('termux');
|
|
107
106
|
|
|
108
107
|
const termux = new Termux(quest);
|
|
109
|
-
|
|
108
|
+
await termux.init();
|
|
110
109
|
|
|
111
110
|
const promises = [];
|
|
112
111
|
for (const ctx of themeContexts) {
|
|
@@ -119,13 +118,13 @@ Goblin.registerQuest(goblinName, 'create', function* (
|
|
|
119
118
|
);
|
|
120
119
|
config.feeds.push(composerId);
|
|
121
120
|
}
|
|
122
|
-
|
|
121
|
+
await Promise.all(promises);
|
|
123
122
|
|
|
124
123
|
const id = quest.goblin.id;
|
|
125
124
|
|
|
126
125
|
quest.goblin.defer(
|
|
127
|
-
quest.sub('goblin.released',
|
|
128
|
-
|
|
126
|
+
quest.sub('goblin.released', async (err, {msg, resp}) => {
|
|
127
|
+
await resp.cmd('laboratory.del', {
|
|
129
128
|
id,
|
|
130
129
|
widgetId: msg.data.id,
|
|
131
130
|
});
|
|
@@ -135,8 +134,8 @@ Goblin.registerQuest(goblinName, 'create', function* (
|
|
|
135
134
|
quest.goblin.defer(
|
|
136
135
|
quest.sub(
|
|
137
136
|
`*::theme-composer@*.${clientSessionId}.reload-theme.requested`,
|
|
138
|
-
|
|
139
|
-
|
|
137
|
+
async (err, {msg, resp}) => {
|
|
138
|
+
await resp.cmd('laboratory.reload-theme', {...msg.data, id});
|
|
140
139
|
}
|
|
141
140
|
)
|
|
142
141
|
);
|
|
@@ -164,19 +163,16 @@ Goblin.registerQuest(goblinName, 'create', function* (
|
|
|
164
163
|
})
|
|
165
164
|
);
|
|
166
165
|
|
|
167
|
-
quest.doSync(
|
|
168
|
-
{id: quest.goblin.id, feed, wid: winId, url, config},
|
|
169
|
-
next.parallel()
|
|
170
|
-
);
|
|
166
|
+
await quest.doSync({id: quest.goblin.id, feed, wid: winId, url, config});
|
|
171
167
|
|
|
172
|
-
|
|
173
|
-
|
|
168
|
+
await quest.me.initZoom({clientSessionId});
|
|
169
|
+
await quest.me.initTheme({clientSessionId});
|
|
174
170
|
|
|
175
171
|
quest.goblin.defer(
|
|
176
172
|
quest.sub.local(
|
|
177
173
|
`*::${winId}.${clientSessionId}.<window-closed>`,
|
|
178
|
-
|
|
179
|
-
|
|
174
|
+
async (err, {msg, resp}) => {
|
|
175
|
+
await resp.cmd('laboratory.close', {id});
|
|
180
176
|
}
|
|
181
177
|
)
|
|
182
178
|
);
|
|
@@ -184,8 +180,8 @@ Goblin.registerQuest(goblinName, 'create', function* (
|
|
|
184
180
|
quest.goblin.defer(
|
|
185
181
|
quest.sub.local(
|
|
186
182
|
`*::${winId}.${clientSessionId}.<window-state-changed>`,
|
|
187
|
-
|
|
188
|
-
|
|
183
|
+
async (err, {msg, resp}) => {
|
|
184
|
+
await resp.cmd('laboratory.save-window-state', {
|
|
189
185
|
id,
|
|
190
186
|
winId,
|
|
191
187
|
state: msg.data.state,
|
|
@@ -193,7 +189,7 @@ Goblin.registerQuest(goblinName, 'create', function* (
|
|
|
193
189
|
}
|
|
194
190
|
)
|
|
195
191
|
);
|
|
196
|
-
|
|
192
|
+
await quest.create('wm', {
|
|
197
193
|
id: winId,
|
|
198
194
|
desktopId,
|
|
199
195
|
url,
|
|
@@ -211,24 +207,19 @@ Goblin.registerQuest(goblinName, 'create', function* (
|
|
|
211
207
|
},
|
|
212
208
|
});
|
|
213
209
|
|
|
214
|
-
/*const titlebarInfos = yield win.getTitlebar();
|
|
215
|
-
if (titlebarInfos) {
|
|
216
|
-
const {titlebar, titlebarId} = titlebarInfos;
|
|
217
|
-
yield quest.me.setTitlebar({titlebar, titlebarId});
|
|
218
|
-
}*/
|
|
219
210
|
quest.log.info(() => `Laboratory ${quest.goblin.id} created!`);
|
|
220
211
|
return quest.goblin.id;
|
|
221
212
|
});
|
|
222
213
|
|
|
223
|
-
Goblin.registerQuest(goblinName, 'close', function
|
|
214
|
+
Goblin.registerQuest(goblinName, 'close', async function (quest) {
|
|
224
215
|
const clientSessionId = quest.goblin.getX('clientSessionId');
|
|
225
216
|
const labId = quest.goblin.id;
|
|
226
217
|
|
|
227
|
-
|
|
218
|
+
await quest.cmd('client-session.close-window', {
|
|
228
219
|
id: clientSessionId,
|
|
229
220
|
winId: `wm@${labId}`,
|
|
230
221
|
});
|
|
231
|
-
|
|
222
|
+
await quest.cmd('client.close-window', {labId});
|
|
232
223
|
quest.release(labId);
|
|
233
224
|
});
|
|
234
225
|
|
|
@@ -244,17 +235,17 @@ Goblin.registerQuest(goblinName, 'get-win-feed', function (quest) {
|
|
|
244
235
|
};
|
|
245
236
|
});
|
|
246
237
|
|
|
247
|
-
Goblin.registerQuest(goblinName, 'set-feed', function
|
|
238
|
+
Goblin.registerQuest(goblinName, 'set-feed', async function (quest, desktopId) {
|
|
248
239
|
quest.goblin.setX('desktopId', desktopId);
|
|
249
240
|
const feeds = quest.goblin.getState().get('feeds');
|
|
250
241
|
const wm = quest.getAPI(`wm@${quest.goblin.id}`);
|
|
251
|
-
|
|
242
|
+
await wm.feedSub({desktopId, feeds: feeds.valueSeq().toArray()});
|
|
252
243
|
|
|
253
244
|
const labId = quest.goblin.id;
|
|
254
245
|
const clientSessionId = quest.goblin.getState().get('clientSessionId');
|
|
255
246
|
const fromFeed = quest.goblin.getState().get('feed');
|
|
256
247
|
for (const branch of [labId, clientSessionId].filter((id) => !!id)) {
|
|
257
|
-
|
|
248
|
+
await quest.warehouse.graft({
|
|
258
249
|
branch,
|
|
259
250
|
fromFeed,
|
|
260
251
|
toFeed: desktopId,
|
|
@@ -262,7 +253,7 @@ Goblin.registerQuest(goblinName, 'set-feed', function* (quest, desktopId) {
|
|
|
262
253
|
}
|
|
263
254
|
|
|
264
255
|
quest.do();
|
|
265
|
-
|
|
256
|
+
await quest.warehouse.resend({feed: desktopId});
|
|
266
257
|
});
|
|
267
258
|
|
|
268
259
|
Goblin.registerQuest(goblinName, 'set-titlebar', function (
|
|
@@ -281,11 +272,11 @@ Goblin.registerQuest(goblinName, 'get-url', function (quest) {
|
|
|
281
272
|
return quest.goblin.getX('url');
|
|
282
273
|
});
|
|
283
274
|
|
|
284
|
-
Goblin.registerQuest(goblinName, 'duplicate', function
|
|
275
|
+
Goblin.registerQuest(goblinName, 'duplicate', async function (quest, forId) {
|
|
285
276
|
const state = quest.goblin.getState();
|
|
286
277
|
const url = state.get('url');
|
|
287
278
|
const newLabId = `laboratory@${quest.uuidV4()}`;
|
|
288
|
-
const lab =
|
|
279
|
+
const lab = await quest.createFor(forId, forId, newLabId, {
|
|
289
280
|
id: newLabId,
|
|
290
281
|
url,
|
|
291
282
|
});
|
|
@@ -324,7 +315,7 @@ Goblin.registerQuest(goblinName, 'set-root', function (
|
|
|
324
315
|
quest.do();
|
|
325
316
|
});
|
|
326
317
|
|
|
327
|
-
Goblin.registerQuest(goblinName, 'listen', function
|
|
318
|
+
Goblin.registerQuest(goblinName, 'listen', async function (
|
|
328
319
|
quest,
|
|
329
320
|
desktopId,
|
|
330
321
|
userId,
|
|
@@ -338,14 +329,14 @@ Goblin.registerQuest(goblinName, 'listen', function* (
|
|
|
338
329
|
|
|
339
330
|
if (userId) {
|
|
340
331
|
const wmAPI = quest.getAPI(`wm@${quest.goblin.id}`);
|
|
341
|
-
|
|
332
|
+
await wmAPI.setUserId({userId});
|
|
342
333
|
}
|
|
343
334
|
|
|
344
335
|
const labId = quest.goblin.id;
|
|
345
336
|
quest.goblin.setX(
|
|
346
337
|
`nav-unsub`,
|
|
347
|
-
quest.sub(`*::<${desktopId}>.nav.requested`,
|
|
348
|
-
|
|
338
|
+
quest.sub(`*::<${desktopId}>.nav.requested`, async (err, {msg, resp}) => {
|
|
339
|
+
await resp.cmd('laboratory.nav', {
|
|
349
340
|
id: labId,
|
|
350
341
|
desktopId,
|
|
351
342
|
...msg.data,
|
|
@@ -355,28 +346,28 @@ Goblin.registerQuest(goblinName, 'listen', function* (
|
|
|
355
346
|
|
|
356
347
|
quest.goblin.setX(
|
|
357
348
|
`change-theme-unsub`,
|
|
358
|
-
quest.sub(
|
|
359
|
-
|
|
360
|
-
{msg, resp}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
}
|
|
366
|
-
|
|
349
|
+
quest.sub(
|
|
350
|
+
`*::<${desktopId}>.change-theme.requested`,
|
|
351
|
+
async (err, {msg, resp}) => {
|
|
352
|
+
await resp.cmd('laboratory.change-theme', {
|
|
353
|
+
id: labId,
|
|
354
|
+
...msg.data,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
)
|
|
367
358
|
);
|
|
368
359
|
|
|
369
360
|
quest.goblin.setX(
|
|
370
361
|
`dispatch-unsub`,
|
|
371
|
-
quest.sub(
|
|
372
|
-
|
|
373
|
-
{msg, resp}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
}
|
|
379
|
-
|
|
362
|
+
quest.sub(
|
|
363
|
+
`*::<${desktopId}>.dispatch.requested`,
|
|
364
|
+
async (err, {msg, resp}) => {
|
|
365
|
+
await resp.cmd('laboratory.dispatch', {
|
|
366
|
+
id: labId,
|
|
367
|
+
...msg.data,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
)
|
|
380
371
|
);
|
|
381
372
|
});
|
|
382
373
|
|
|
@@ -391,29 +382,32 @@ function unlisten(quest) {
|
|
|
391
382
|
}
|
|
392
383
|
}
|
|
393
384
|
|
|
394
|
-
Goblin.registerQuest(goblinName, 'nav', function
|
|
385
|
+
Goblin.registerQuest(goblinName, 'nav', async function (quest, route) {
|
|
395
386
|
const win = quest.getAPI(`wm@${quest.goblin.id}`);
|
|
396
|
-
|
|
387
|
+
await win.nav({route});
|
|
397
388
|
});
|
|
398
389
|
|
|
399
390
|
/************************ SETTINGS *********************************/
|
|
400
391
|
|
|
401
|
-
Goblin.registerQuest(goblinName, 'save-settings', function
|
|
392
|
+
Goblin.registerQuest(goblinName, 'save-settings', async function (
|
|
393
|
+
quest,
|
|
394
|
+
propertie
|
|
395
|
+
) {
|
|
402
396
|
const value = quest.goblin.getState().get(propertie);
|
|
403
397
|
const clientSessionId = quest.goblin.getX('clientSessionId');
|
|
404
|
-
|
|
398
|
+
await quest.cmd(`client-session.set-${propertie}`, {
|
|
405
399
|
id: clientSessionId,
|
|
406
400
|
[propertie]: value,
|
|
407
401
|
});
|
|
408
402
|
});
|
|
409
403
|
|
|
410
|
-
Goblin.registerQuest(goblinName, 'save-window-state', function
|
|
404
|
+
Goblin.registerQuest(goblinName, 'save-window-state', async function (
|
|
411
405
|
quest,
|
|
412
406
|
winId,
|
|
413
407
|
state
|
|
414
408
|
) {
|
|
415
409
|
const clientSessionId = quest.goblin.getX('clientSessionId');
|
|
416
|
-
|
|
410
|
+
await quest.cmd(`client-session.set-window-state`, {
|
|
417
411
|
id: clientSessionId,
|
|
418
412
|
winId,
|
|
419
413
|
state,
|
|
@@ -422,22 +416,22 @@ Goblin.registerQuest(goblinName, 'save-window-state', function* (
|
|
|
422
416
|
|
|
423
417
|
/******************************************************************************/
|
|
424
418
|
|
|
425
|
-
Goblin.registerQuest(goblinName, 'init-theme', function
|
|
419
|
+
Goblin.registerQuest(goblinName, 'init-theme', async function (
|
|
426
420
|
quest,
|
|
427
421
|
clientSessionId
|
|
428
422
|
) {
|
|
429
|
-
const name =
|
|
423
|
+
const name = await quest.cmd('client-session.get-theme', {
|
|
430
424
|
id: clientSessionId,
|
|
431
425
|
});
|
|
432
426
|
|
|
433
427
|
if (name) {
|
|
434
|
-
|
|
428
|
+
await quest.me.changeTheme({name});
|
|
435
429
|
}
|
|
436
430
|
});
|
|
437
431
|
|
|
438
|
-
Goblin.registerQuest(goblinName, 'change-theme', function
|
|
432
|
+
Goblin.registerQuest(goblinName, 'change-theme', async function (quest, name) {
|
|
439
433
|
quest.do({name});
|
|
440
|
-
|
|
434
|
+
await quest.me.saveSettings({propertie: 'theme'});
|
|
441
435
|
});
|
|
442
436
|
|
|
443
437
|
Goblin.registerQuest(goblinName, 'reload-theme', function (quest, name) {
|
|
@@ -446,49 +440,47 @@ Goblin.registerQuest(goblinName, 'reload-theme', function (quest, name) {
|
|
|
446
440
|
|
|
447
441
|
/******************************************************************************/
|
|
448
442
|
|
|
449
|
-
Goblin.registerQuest(goblinName, 'init-zoom', function
|
|
443
|
+
Goblin.registerQuest(goblinName, 'init-zoom', async function (
|
|
450
444
|
quest,
|
|
451
445
|
clientSessionId
|
|
452
446
|
) {
|
|
453
|
-
let zoom =
|
|
447
|
+
let zoom = await quest.cmd('client-session.get-zoom', {
|
|
454
448
|
id: clientSessionId,
|
|
455
449
|
});
|
|
456
450
|
|
|
457
|
-
|
|
458
|
-
zoom,
|
|
459
|
-
});
|
|
451
|
+
await quest.me.setZoom({zoom});
|
|
460
452
|
});
|
|
461
453
|
|
|
462
|
-
Goblin.registerQuest(goblinName, 'set-zoom', function
|
|
454
|
+
Goblin.registerQuest(goblinName, 'set-zoom', async function (quest) {
|
|
463
455
|
quest.do();
|
|
464
|
-
|
|
456
|
+
await quest.me.saveSettings({propertie: 'zoom'});
|
|
465
457
|
});
|
|
466
458
|
|
|
467
|
-
Goblin.registerQuest(goblinName, 'zoom', function
|
|
459
|
+
Goblin.registerQuest(goblinName, 'zoom', async function (quest) {
|
|
468
460
|
quest.do();
|
|
469
|
-
|
|
461
|
+
await quest.me.saveSettings({propertie: 'zoom'});
|
|
470
462
|
});
|
|
471
463
|
|
|
472
|
-
Goblin.registerQuest(goblinName, 'un-zoom', function
|
|
464
|
+
Goblin.registerQuest(goblinName, 'un-zoom', async function (quest) {
|
|
473
465
|
quest.do();
|
|
474
|
-
|
|
466
|
+
await quest.me.saveSettings({propertie: 'zoom'});
|
|
475
467
|
});
|
|
476
468
|
|
|
477
|
-
Goblin.registerQuest(goblinName, 'default-zoom', function
|
|
469
|
+
Goblin.registerQuest(goblinName, 'default-zoom', async function (quest) {
|
|
478
470
|
quest.do();
|
|
479
|
-
|
|
471
|
+
await quest.me.saveSettings({propertie: 'zoom'});
|
|
480
472
|
});
|
|
481
473
|
|
|
482
|
-
Goblin.registerQuest(goblinName, 'change-zoom', function
|
|
474
|
+
Goblin.registerQuest(goblinName, 'change-zoom', async function (quest) {
|
|
483
475
|
quest.do();
|
|
484
|
-
|
|
476
|
+
await quest.me.saveSettings({propertie: 'zoom'});
|
|
485
477
|
});
|
|
486
478
|
|
|
487
479
|
/******************************************************************************/
|
|
488
480
|
|
|
489
|
-
Goblin.registerQuest(goblinName, 'dispatch', function
|
|
481
|
+
Goblin.registerQuest(goblinName, 'dispatch', async function (quest, action) {
|
|
490
482
|
const win = quest.getAPI(`wm@${quest.goblin.id}`);
|
|
491
|
-
|
|
483
|
+
await win.dispatch({action});
|
|
492
484
|
});
|
|
493
485
|
|
|
494
486
|
Goblin.registerQuest(goblinName, 'open', function (quest, route) {
|
|
@@ -496,7 +488,7 @@ Goblin.registerQuest(goblinName, 'open', function (quest, route) {
|
|
|
496
488
|
quest.log.info(route);
|
|
497
489
|
});
|
|
498
490
|
|
|
499
|
-
Goblin.registerQuest(goblinName, 'del', function
|
|
491
|
+
Goblin.registerQuest(goblinName, 'del', async function (quest, widgetId) {
|
|
500
492
|
const state = quest.goblin.getState();
|
|
501
493
|
const feed = state.get('feed');
|
|
502
494
|
const branch = widgetId;
|
|
@@ -506,12 +498,12 @@ Goblin.registerQuest(goblinName, 'del', function* (quest, widgetId) {
|
|
|
506
498
|
if (quest.goblin.getX('forceDispose')) {
|
|
507
499
|
const WM = require('xcraft-core-host/lib/wm.js').instance;
|
|
508
500
|
WM.disposeAll();
|
|
509
|
-
quest.cmd('shutdown'); /* no
|
|
501
|
+
quest.cmd('shutdown'); /* no await here because it's terminated */
|
|
510
502
|
return;
|
|
511
503
|
}
|
|
512
504
|
|
|
513
505
|
if (branch === feed || (branch === labId && useConfigurator === false)) {
|
|
514
|
-
|
|
506
|
+
await quest.warehouse.unsubscribe({feed: branch});
|
|
515
507
|
} else {
|
|
516
508
|
quest.log.info(
|
|
517
509
|
`Laboratory deleting widget ${widgetId} from window ${feed}`
|
|
@@ -523,7 +515,7 @@ Goblin.registerQuest(goblinName, 'del', function* (quest, widgetId) {
|
|
|
523
515
|
if (branch === labId) {
|
|
524
516
|
parents.push(`goblin-orc@*`);
|
|
525
517
|
}
|
|
526
|
-
|
|
518
|
+
await quest.warehouse.feedSubscriptionDel({feed, branch, parents});
|
|
527
519
|
}
|
|
528
520
|
});
|
|
529
521
|
|
package/widgets/widget/index.js
CHANGED
|
@@ -29,6 +29,8 @@ const throttle250 = _.throttle((fct) => fct(), 250);
|
|
|
29
29
|
// }
|
|
30
30
|
|
|
31
31
|
class Widget extends React.Component {
|
|
32
|
+
static #nameCache = new Map();
|
|
33
|
+
|
|
32
34
|
constructor() {
|
|
33
35
|
super(...arguments);
|
|
34
36
|
this._names = this._getInheritedNames();
|
|
@@ -44,7 +46,14 @@ class Widget extends React.Component {
|
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
static getWidgetName(constructorName) {
|
|
47
|
-
|
|
49
|
+
if (this.#nameCache.has(constructorName)) {
|
|
50
|
+
return this.#nameCache.get(constructorName);
|
|
51
|
+
}
|
|
52
|
+
const name = constructorName
|
|
53
|
+
.replace(/([a-z])([A-Z])/g, '$1-$2')
|
|
54
|
+
.toLowerCase();
|
|
55
|
+
this.#nameCache.set(constructorName, name);
|
|
56
|
+
return name;
|
|
48
57
|
}
|
|
49
58
|
|
|
50
59
|
_getInheritedNames() {
|
|
@@ -305,7 +314,8 @@ class Widget extends React.Component {
|
|
|
305
314
|
}
|
|
306
315
|
/** @deprecated Replace by doFor.
|
|
307
316
|
* It's possible to have a mismatch between service name and serviceId.
|
|
308
|
-
* Prefer to use doFor with the service id.
|
|
317
|
+
* Prefer to use doFor with the service id.
|
|
318
|
+
*/
|
|
309
319
|
doAs(service, action, args) {
|
|
310
320
|
const id = this.props.id || this.context.id;
|
|
311
321
|
if (!id) {
|