This commit is contained in:
Arthur Ria
2026-05-20 09:47:25 +02:00
commit 6484dd09c5
12 changed files with 1340 additions and 0 deletions
+364
View File
@@ -0,0 +1,364 @@
# CLAUDE.md - TakeID Browser Extension
Ce fichier guide Claude Code pour le développement de l'extension TakeID.
## Objectif
Extension navigateur (Chrome + Firefox) qui récupère les **IDs internes (GUIDs)** des éléments sélectionnés dans les vues SmartUI du WMS Mecalux EasyWMS, et les copie dans le presse-papier.
## Contexte
SmartUI est l'interface web du WMS EasyWMS (Mecalux). Elle affiche des listes d'entités (articles, commandes, conteneurs...) dans des grilles. Quand l'utilisateur sélectionne des lignes via les checkboxes, il n'y a **aucun moyen visible** de récupérer l'ID interne (GUID) de ces éléments - il n'est pas dans le DOM.
Cependant, l'investigation réseau a révélé que SmartUI charge les données via un appel XHR `POST getDataViewList` dont la réponse contient les IDs de chaque ligne.
## Architecture
### Principe
1. L'extension intercepte les réponses XHR `getDataViewList` de SmartUI
2. Elle extrait et met en cache le mapping `code -> GUID` pour chaque page
3. Quand l'utilisateur clique sur le bouton de l'extension, elle lit les lignes sélectionnées dans le DOM
4. Elle résout les GUIDs via le cache et les copie dans le presse-papier (un par ligne)
### Structure des fichiers
```
takeid-extension/
manifest.json # Manifest V3 (Chrome) compatible Firefox
popup/
popup.html # UI du popup (bouton copier, liste des IDs)
popup.js # Logique du popup
popup.css # Styles du popup
content/
content-script.js # Content script - pont entre injected.js et popup
injected.js # Script injecté dans le contexte page - intercepte les XHR
icons/
icon-16.png
icon-32.png
icon-48.png
icon-128.png
CLAUDE.md # Ce fichier
```
### Flux de données
```
[Page SmartUI]
|
v
SmartUI fait XHR POST vers getDataViewList
|
v
injected.js intercepte la réponse (monkey-patch XMLHttpRequest)
| Parse JSON -> extrait datos[].id.value et datos[].code.value
|
v (window.postMessage)
content-script.js reçoit le mapping et le stocke en mémoire
|
v (quand l'utilisateur clique sur l'icône de l'extension)
popup.js demande les IDs sélectionnés au content-script (chrome.tabs.sendMessage)
|
v
content-script.js lit le DOM pour identifier les lignes cochées
| Extrait le code de chaque ligne sélectionnée
| Résout code -> GUID via le cache
|
v
popup.js reçoit les GUIDs et les copie dans le presse-papier
```
## Données techniques SmartUI
### URL Pattern
Les vues SmartUI ont cette forme d'URL :
```
https://<HOST>/SmartUI/smartui?viewname=<AppName>|<ViewName>&viewtype=ViewList
```
Exemples :
- `https://10.255.255.2/SmartUI/smartui?viewname=CustomApplication|ProductsVList&viewtype=ViewList`
- Le host peut varier (IP interne, domaine...)
### Endpoint intercepté : getDataViewList
**Requête :** POST vers une URL contenant `getDataViewList`
(L'URL exacte est relative au SmartUI Hub, ex: `https://<HOST>/SmartUI/smartuihubcore?...` via SignalR ou direct XHR)
**Réponse JSON :**
```json
{
"datos": [
{
"id": {
"isResource": false,
"keyResource": null,
"value": "add2a893-5364-4c8e-8683-6a611a475ad6",
"cssClass": "None",
"applicationBase": null
},
"code": {
"url": "viewname=CustomApplication|ProductsVList&viewtype=ViewList&selectedKey=code&selectedValue=*000007713009750",
"isDataObjectUrl": 1,
"isResource": false,
"keyResource": null,
"value": "*000007713009750",
"cssClass": "None",
"applicationBase": null
},
"description": {
"value": "sous-table Halo mela 1 niv L1400 2 tiroirs double"
}
}
],
"file": null,
"count": 0
}
```
**Points clés :**
- `datos` est un tableau de lignes (50 par page)
- Chaque champ est un objet avec une propriété `value`
- L'ID est dans `datos[i].id.value` (GUID, ex: `add2a893-5364-4c8e-8683-6a611a475ad6`)
- Le code est dans `datos[i].code.value` (ex: `*000007713009750`)
- Certains champs simples sont des scalaires directs (pas d'objet wrapper)
- Le champ `id` n'est pas toujours un objet - il peut être un scalaire string directement
### Endpoint de métadonnées : view
La réponse `view` (POST) fournit les métadonnées de la vue :
```json
{
"primaryKeyName": "id",
"entity": {
"tableName": "ProductViewV2",
"pluralName": "Articles",
"properties": [...]
},
"viewGrid": {
"viewFields": [...]
},
"name": "ProductsVList",
"applicationName": "CustomApplication",
"title": "Articles"
}
```
**Utilité :** `primaryKeyName` confirme le nom du champ PK (toujours "id" pour les vues standard). L'extension peut intercepter ce call pour connaître le `primaryKeyName` dynamiquement.
### Structure DOM de la grille SmartUI
**IMPORTANT : La structure DOM exacte doit être vérifiée pendant le développement.**
Indices connus :
- SmartUI utilise un framework JavaScript (probablement Knockout.js ou similaire)
- Les lignes sélectionnées ont des checkboxes cochées
- Le compteur "Sélectionné:3" est visible en haut de la grille
- Les codes sont des liens cliquables dans la première colonne de données
- La grille a des colonnes : checkbox, Code, Description courte, UdM de base, Type, Profil logistique, Classification...
**Stratégie de détection des lignes sélectionnées :**
L'approche recommandée est d'inspecter le DOM pour trouver les lignes sélectionnées. Chercher :
1. Des `<tr>` ou `<div>` avec une classe comme `selected`, `checked`, `active`, `k-state-selected`
2. Des `<input type="checkbox">` cochés dans la colonne de sélection
3. Des attributs `aria-selected="true"`
Puis extraire le code depuis la même ligne (première colonne de lien, ou cellule avec le texte du code).
**Approche alternative (plus robuste) :** Au lieu de lire le code depuis le DOM, l'extension peut utiliser l'**index de la ligne** dans la grille pour le mapper aux données du cache `getDataViewList`. L'ordre des lignes dans le DOM correspond à l'ordre des éléments dans `datos[]`.
## Compatibilité Chrome + Firefox
### Manifest V3
Le `manifest.json` doit être compatible Chrome ET Firefox :
```json
{
"manifest_version": 3,
"name": "TakeID - WMS SmartUI ID Grabber",
"version": "1.0.0",
"description": "Copie les IDs internes des éléments sélectionnés dans les vues SmartUI du WMS EasyWMS",
"permissions": ["activeTab", "clipboardWrite", "scripting"],
"host_permissions": ["*://*/*"],
"action": {
"default_popup": "popup/popup.html",
"default_icon": {
"16": "icons/icon-16.png",
"32": "icons/icon-32.png",
"48": "icons/icon-48.png",
"128": "icons/icon-128.png"
}
},
"content_scripts": [
{
"matches": ["*://*/*SmartUI/*"],
"js": ["content/content-script.js"],
"run_at": "document_start"
}
],
"icons": {
"16": "icons/icon-16.png",
"32": "icons/icon-32.png",
"48": "icons/icon-48.png",
"128": "icons/icon-128.png"
},
"browser_specific_settings": {
"gecko": {
"id": "takeid@mecalux.local",
"strict_min_version": "109.0"
}
}
}
```
### Différences Chrome vs Firefox
- **API namespace :** Utiliser `browser` (Firefox natif) avec fallback sur `chrome` (Chrome). Ajouter un polyfill en haut de chaque script :
```javascript
const browser = globalThis.browser || globalThis.chrome;
```
- **Manifest V3 :** Firefox supporte MV3 depuis v109. La clé `browser_specific_settings.gecko` est requise pour Firefox.
- **clipboardWrite :** Fonctionne dans les deux navigateurs via l'API `navigator.clipboard.writeText()` dans le popup (qui a un contexte sécurisé).
- **Content script injection :** `world: "MAIN"` n'est pas supporté partout sur Firefox. Utiliser l'injection via `<script>` tag dans le content script pour injecter `injected.js` dans le contexte page.
## Implémentation détaillée
### injected.js - Interception XHR
Ce script est injecté dans le contexte de la page (pas le monde isolé du content script).
```javascript
// Monkey-patch XMLHttpRequest pour intercepter les réponses getDataViewList
(function() {
const originalOpen = XMLHttpRequest.prototype.open;
const originalSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function(method, url, ...args) {
this._takeid_url = url;
return originalOpen.call(this, method, url, ...args);
};
XMLHttpRequest.prototype.send = function(body) {
if (this._takeid_url && this._takeid_url.includes('getDataViewList')) {
this.addEventListener('load', function() {
try {
const data = JSON.parse(this.responseText);
if (data && data.datos && Array.isArray(data.datos)) {
// Extraire le mapping
const items = data.datos.map((row, index) => {
const id = extractValue(row.id);
const code = extractValue(row.code);
return { index, id, code };
}).filter(item => item.id);
// Envoyer au content script
window.postMessage({
type: 'TAKEID_DATA',
items: items
}, '*');
}
} catch (e) {
// Silently ignore parse errors
}
});
}
return originalSend.call(this, body);
};
function extractValue(field) {
if (!field) return null;
if (typeof field === 'string') return field;
if (typeof field === 'object' && field.value !== undefined) return field.value;
return null;
}
})();
```
**Note :** Il faut aussi patcher `fetch()` si SmartUI l'utilise (vérifier pendant le développement). Il faut aussi surveiller les réponses qui arrivent via SignalR/WebSocket (le endpoint `smartuihubcore`). Dans ce cas, intercepter les messages WebSocket.
### content-script.js - Cache et lecture DOM
```javascript
// Cache des données
let dataCache = []; // [{index, id, code}, ...]
// Écouter les messages de injected.js
window.addEventListener('message', (event) => {
if (event.data && event.data.type === 'TAKEID_DATA') {
dataCache = event.data.items;
}
});
// Injecter injected.js dans le contexte page
const script = document.createElement('script');
script.src = browser.runtime.getURL('content/injected.js');
document.documentElement.appendChild(script);
// Répondre aux messages du popup
browser.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg.type === 'GET_SELECTED_IDS') {
const ids = getSelectedIds();
sendResponse({ ids });
}
});
function getSelectedIds() {
// TODO: Implémenter la lecture des lignes sélectionnées dans le DOM
// Stratégie: trouver les checkboxes cochées, identifier leur index de ligne,
// mapper vers dataCache[index].id
}
```
### popup.js - Interface utilisateur
Le popup affiche :
- Nombre d'éléments sélectionnés
- Liste des IDs (preview)
- Bouton "Copier"
- Message de confirmation
## Commandes de développement
```bash
# Pas de build nécessaire - vanilla JS
# Tester sur Chrome : chrome://extensions -> Mode développeur -> Charger l'extension non empaquetée
# Tester sur Firefox : about:debugging -> Ce Firefox -> Charger un module temporaire -> manifest.json
```
## Contraintes
1. **Pas de dépendances externes** - Vanilla JS uniquement, pas de npm/build
2. **Pas de credentials** - L'extension n'a pas besoin de s'authentifier, elle intercepte les données qui transitent déjà
3. **Pagination** - Le cache est rafraîchi à chaque chargement de page de données (50 lignes). Quand l'utilisateur change de page dans la grille, un nouveau `getDataViewList` est émis et le cache est remplacé
4. **Multi-vues** - L'extension doit fonctionner sur n'importe quelle vue SmartUI (Products, OutboundOrders, Containers...), pas seulement les Articles
5. **Performance** - Le monkey-patching ne doit pas ralentir SmartUI. Ne parser que les réponses `getDataViewList`
6. **Icônes** - Générer des icônes SVG simples en placeholder (un "ID" stylisé ou un presse-papier)
## Bugs corrigés (post-MVP)
### Bug 1 - getDataViewList via SignalR (body, pas URL)
SmartUI envoie les appels `getDataViewList` via un hub SignalR (`smartuihubcore`). Le nom de méthode `getDataViewList` est dans le **body** de la requête, pas dans l'URL. Le check initial `url.indexOf('getDataViewList')` échouait silencieusement.
**Fix dans injected.js :** La fonction `isTargetRequest(url, body)` vérifie maintenant l'URL ET le body de la requête pour détecter `getDataViewList`.
### Bug 2 - Cache écrasé par une réponse vide
SmartUI fait **deux** appels `getDataViewList` : un avec les 50 lignes de données, et un second (count/metadata) qui retourne `datos: []`. Le second écrasait le cache avec 0 items.
**Fix dans content-script.js :** On ignore les réponses avec `items.length === 0` pour ne pas écraser un cache valide.
### Bug 3 - event.source !== window sur Firefox
Les content scripts Firefox utilisent un "Xray wrapper". `event.source` (page réelle) n'est pas strictement égal à `window` (wrapper), donc le check `event.source !== window` bloquait la réception des postMessage.
**Fix dans content-script.js :** Suppression du check `event.source !== window`. Le filtrage se fait uniquement via `data.type === 'TAKEID_DATA'`.
## Priorités de développement
1. **Phase 1 - MVP :** ✅ Interception XHR + cache + lecture DOM + copie clipboard. Testé sur les vues Articles et Stock.
2. **Phase 2 - Polish :** Icônes, messages d'état, gestion des cas limites (pas de sélection, cache vide, page sans SmartUI).
3. **Phase 3 - Robustesse :** Support WebSocket/SignalR si certaines vues chargent les données autrement. Support multi-onglets.