v0.2
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user