365 lines
13 KiB
Markdown
365 lines
13 KiB
Markdown
# 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.
|