13 KiB
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
- L'extension intercepte les réponses XHR
getDataViewListde SmartUI - Elle extrait et met en cache le mapping
code -> GUIDpour chaque page - Quand l'utilisateur clique sur le bouton de l'extension, elle lit les lignes sélectionnées dans le DOM
- 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 :
{
"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 :
datosest 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
idn'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 :
{
"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 :
- Des
<tr>ou<div>avec une classe commeselected,checked,active,k-state-selected - Des
<input type="checkbox">cochés dans la colonne de sélection - 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 :
{
"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 surchrome(Chrome). Ajouter un polyfill en haut de chaque script :const browser = globalThis.browser || globalThis.chrome; - Manifest V3 : Firefox supporte MV3 depuis v109. La clé
browser_specific_settings.geckoest 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 injecterinjected.jsdans 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).
// 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
// 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
# 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
- Pas de dépendances externes - Vanilla JS uniquement, pas de npm/build
- Pas de credentials - L'extension n'a pas besoin de s'authentifier, elle intercepte les données qui transitent déjà
- 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
getDataViewListest émis et le cache est remplacé - Multi-vues - L'extension doit fonctionner sur n'importe quelle vue SmartUI (Products, OutboundOrders, Containers...), pas seulement les Articles
- Performance - Le monkey-patching ne doit pas ralentir SmartUI. Ne parser que les réponses
getDataViewList - 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
- Phase 1 - MVP : ✅ Interception XHR + cache + lecture DOM + copie clipboard. Testé sur les vues Articles et Stock.
- Phase 2 - Polish : Icônes, messages d'état, gestion des cas limites (pas de sélection, cache vide, page sans SmartUI).
- Phase 3 - Robustesse : Support WebSocket/SignalR si certaines vues chargent les données autrement. Support multi-onglets.