Files
Arthur Ria 6484dd09c5 v0.2
2026-05-20 09:47:25 +02:00

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

  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 :

{
  "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 :

{
  "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 :

{
  "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 :
    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).

// 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

  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.