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.
|
||||
@@ -0,0 +1,223 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const browser = globalThis.browser || globalThis.chrome;
|
||||
|
||||
if (window.__takeid_content_script__) return;
|
||||
window.__takeid_content_script__ = true;
|
||||
|
||||
// Cache des items recus du script injecte: [{index, id, code}]
|
||||
let dataCache = [];
|
||||
let lastCacheTimestamp = 0;
|
||||
|
||||
// ----- Injection de injected.js dans le contexte page -----
|
||||
function injectPageScript() {
|
||||
try {
|
||||
const script = document.createElement('script');
|
||||
script.src = browser.runtime.getURL('content/injected.js');
|
||||
script.async = false;
|
||||
script.onload = function () {
|
||||
// Une fois execute, on retire l'element pour ne pas polluer le DOM
|
||||
if (script.parentNode) script.parentNode.removeChild(script);
|
||||
};
|
||||
(document.head || document.documentElement).appendChild(script);
|
||||
} catch (e) {
|
||||
// swallow
|
||||
}
|
||||
}
|
||||
injectPageScript();
|
||||
|
||||
// ----- Reception des donnees de injected.js -----
|
||||
// Note: on ne filtre PAS sur event.source car sur Firefox le content script
|
||||
// utilise un Xray wrapper et event.source !== window meme si c'est la meme page.
|
||||
window.addEventListener('message', (event) => {
|
||||
const data = event.data;
|
||||
if (!data || typeof data !== 'object') return;
|
||||
if (data.type !== 'TAKEID_DATA') return;
|
||||
if (!Array.isArray(data.items)) return;
|
||||
|
||||
// Ne pas ecraser le cache avec une reponse vide (certains appels getDataViewList
|
||||
// sont des requetes de count/metadata qui retournent datos=[])
|
||||
if (data.items.length === 0) {
|
||||
console.log('[TakeID] Ignoring empty getDataViewList response (keeping', dataCache.length, 'items in cache)');
|
||||
return;
|
||||
}
|
||||
dataCache = data.items;
|
||||
lastCacheTimestamp = data.timestamp || Date.now();
|
||||
console.log('[TakeID] Cache updated:', dataCache.length, 'items');
|
||||
});
|
||||
|
||||
// ----- Lecture du DOM : trouver les lignes selectionnees -----
|
||||
function normalizeCode(s) {
|
||||
if (s === null || s === undefined) return '';
|
||||
return String(s).trim();
|
||||
}
|
||||
|
||||
function findGridRoot() {
|
||||
// Heuristique : chercher un conteneur de grille
|
||||
const candidates = [
|
||||
'.k-grid',
|
||||
'[role="grid"]',
|
||||
'table.grid',
|
||||
'.smartui-grid',
|
||||
'table'
|
||||
];
|
||||
for (const sel of candidates) {
|
||||
const el = document.querySelector(sel);
|
||||
if (el) return el;
|
||||
}
|
||||
return document;
|
||||
}
|
||||
|
||||
function getAllDataRows(root) {
|
||||
// Recupere toutes les lignes "data" (potentiellement tr ou divs)
|
||||
let rows = root.querySelectorAll('tr[role="row"]');
|
||||
if (rows && rows.length) return Array.from(rows);
|
||||
|
||||
rows = root.querySelectorAll('tbody > tr');
|
||||
if (rows && rows.length) return Array.from(rows);
|
||||
|
||||
rows = root.querySelectorAll('[role="row"]');
|
||||
if (rows && rows.length) return Array.from(rows);
|
||||
|
||||
rows = root.querySelectorAll('.k-grid-content tr');
|
||||
if (rows && rows.length) return Array.from(rows);
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
function isHeaderRow(row) {
|
||||
if (!row) return false;
|
||||
if (row.matches && (row.matches('thead tr') || row.matches('[role="row"][aria-rowindex="1"]'))) return true;
|
||||
// ligne sans cellules <td>, juste des <th>
|
||||
if (row.querySelector && row.querySelector('th') && !row.querySelector('td')) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function rowIsSelected(row) {
|
||||
if (!row) return false;
|
||||
|
||||
// 1. classes communes
|
||||
const selectedClasses = ['selected', 'k-state-selected', 'k-selected', 'is-selected', 'row-selected', 'active'];
|
||||
if (row.classList) {
|
||||
for (const cls of selectedClasses) {
|
||||
if (row.classList.contains(cls)) return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. aria-selected
|
||||
if (row.getAttribute && row.getAttribute('aria-selected') === 'true') return true;
|
||||
|
||||
// 3. checkbox cochee a l'interieur de la ligne
|
||||
const cb = row.querySelector && row.querySelector('input[type="checkbox"]');
|
||||
if (cb && cb.checked) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function extractCodeFromRow(row) {
|
||||
if (!row) return null;
|
||||
|
||||
// 1. data-attribute eventuel
|
||||
const dataAttrs = ['data-code', 'data-key', 'data-id'];
|
||||
for (const a of dataAttrs) {
|
||||
const v = row.getAttribute && row.getAttribute(a);
|
||||
if (v) return normalizeCode(v);
|
||||
}
|
||||
|
||||
// 2. premier lien clickable (souvent la colonne Code dans SmartUI)
|
||||
const link = row.querySelector && row.querySelector('a');
|
||||
if (link && link.textContent) {
|
||||
const t = normalizeCode(link.textContent);
|
||||
if (t) return t;
|
||||
}
|
||||
|
||||
// 3. premiere cellule "non checkbox"
|
||||
const cells = row.querySelectorAll ? row.querySelectorAll('td') : [];
|
||||
for (const cell of cells) {
|
||||
if (cell.querySelector('input[type="checkbox"]')) continue;
|
||||
const t = normalizeCode(cell.textContent);
|
||||
if (t) return t;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function indexOfDataRow(row, allRows) {
|
||||
// Trouve l'index de la ligne dans la sequence des lignes "data" (hors header)
|
||||
const dataRows = allRows.filter((r) => !isHeaderRow(r));
|
||||
return dataRows.indexOf(row);
|
||||
}
|
||||
|
||||
function getSelectedIds() {
|
||||
const root = findGridRoot();
|
||||
const allRows = getAllDataRows(root);
|
||||
const dataRows = allRows.filter((r) => !isHeaderRow(r));
|
||||
|
||||
const selectedRows = dataRows.filter(rowIsSelected);
|
||||
|
||||
// Strategie A : trouver via le code (lookup dans le cache)
|
||||
const cacheByCode = new Map();
|
||||
for (const item of dataCache) {
|
||||
if (item.code) cacheByCode.set(normalizeCode(item.code), item.id);
|
||||
}
|
||||
|
||||
const pairs = [];
|
||||
const seen = new Set();
|
||||
|
||||
for (const row of selectedRows) {
|
||||
let id = null;
|
||||
let resolvedCode = null;
|
||||
|
||||
const code = extractCodeFromRow(row);
|
||||
if (code && cacheByCode.has(code)) {
|
||||
id = cacheByCode.get(code);
|
||||
resolvedCode = code;
|
||||
}
|
||||
|
||||
// Strategie B : par index si le code n'a pas matche
|
||||
if (!id) {
|
||||
const idx = dataRows.indexOf(row);
|
||||
if (idx >= 0 && idx < dataCache.length) {
|
||||
id = dataCache[idx].id;
|
||||
resolvedCode = dataCache[idx].code || code || null;
|
||||
}
|
||||
}
|
||||
|
||||
if (id && !seen.has(id)) {
|
||||
seen.add(id);
|
||||
pairs.push({ id, code: resolvedCode });
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
pairs,
|
||||
ids: pairs.map((p) => p.id),
|
||||
selectedCount: selectedRows.length,
|
||||
cacheSize: dataCache.length,
|
||||
cacheTimestamp: lastCacheTimestamp
|
||||
};
|
||||
}
|
||||
|
||||
// ----- Reception des messages du popup -----
|
||||
browser.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||
if (!msg || typeof msg !== 'object') return false;
|
||||
|
||||
if (msg.type === 'TAKEID_PING') {
|
||||
sendResponse({ ok: true, cacheSize: dataCache.length });
|
||||
return false;
|
||||
}
|
||||
|
||||
if (msg.type === 'GET_SELECTED_IDS') {
|
||||
try {
|
||||
const result = getSelectedIds();
|
||||
sendResponse({ ok: true, ...result });
|
||||
} catch (e) {
|
||||
sendResponse({ ok: false, error: String(e && e.message ? e.message : e) });
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,155 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
if (window.__takeid_injected__) return;
|
||||
window.__takeid_injected__ = true;
|
||||
|
||||
const TARGET_SUBSTR = 'getDataViewList';
|
||||
const LOG_PREFIX = '[TakeID]';
|
||||
|
||||
function extractValue(field) {
|
||||
if (field === null || field === undefined) return null;
|
||||
if (typeof field === 'string') return field;
|
||||
if (typeof field === 'number' || typeof field === 'boolean') return String(field);
|
||||
if (typeof field === 'object') {
|
||||
if (Object.prototype.hasOwnProperty.call(field, 'value')) {
|
||||
return extractValue(field.value);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function parseAndPost(responseText) {
|
||||
if (!responseText || typeof responseText !== 'string') return;
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(responseText);
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
if (!data || !Array.isArray(data.datos)) return;
|
||||
|
||||
const items = data.datos
|
||||
.map((row, index) => {
|
||||
if (!row || typeof row !== 'object') return null;
|
||||
const id = extractValue(row.id);
|
||||
const code = extractValue(row.code);
|
||||
return { index, id, code };
|
||||
})
|
||||
.filter((item) => item && item.id);
|
||||
|
||||
console.log(LOG_PREFIX, 'Posting', items.length, 'items to content script');
|
||||
window.postMessage(
|
||||
{
|
||||
type: 'TAKEID_DATA',
|
||||
items: items,
|
||||
timestamp: Date.now()
|
||||
},
|
||||
'*'
|
||||
);
|
||||
}
|
||||
|
||||
// Verifie si l'URL OU le body contient getDataViewList
|
||||
function isTargetRequest(url, body) {
|
||||
if (url && url.indexOf(TARGET_SUBSTR) !== -1) return true;
|
||||
if (body && typeof body === 'string' && body.indexOf(TARGET_SUBSTR) !== -1) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- Monkey-patch XMLHttpRequest ---
|
||||
const originalOpen = XMLHttpRequest.prototype.open;
|
||||
const originalSend = XMLHttpRequest.prototype.send;
|
||||
|
||||
XMLHttpRequest.prototype.open = function (method, url) {
|
||||
try {
|
||||
this.__takeid_url = typeof url === 'string' ? url : (url && url.toString ? url.toString() : '');
|
||||
} catch (e) {
|
||||
this.__takeid_url = '';
|
||||
}
|
||||
return originalOpen.apply(this, arguments);
|
||||
};
|
||||
|
||||
XMLHttpRequest.prototype.send = function (body) {
|
||||
try {
|
||||
// Convertir le body en string pour la recherche
|
||||
let bodyStr = '';
|
||||
if (body) {
|
||||
if (typeof body === 'string') {
|
||||
bodyStr = body;
|
||||
} else if (body instanceof FormData) {
|
||||
// FormData: on ne peut pas facilement chercher dedans, skip
|
||||
bodyStr = '';
|
||||
} else {
|
||||
try { bodyStr = JSON.stringify(body); } catch (e) { bodyStr = ''; }
|
||||
}
|
||||
}
|
||||
|
||||
if (isTargetRequest(this.__takeid_url, bodyStr)) {
|
||||
console.log(LOG_PREFIX, 'Intercepted getDataViewList - URL:', this.__takeid_url);
|
||||
this.addEventListener('load', function () {
|
||||
try {
|
||||
let text = null;
|
||||
if (this.responseType === '' || this.responseType === 'text') {
|
||||
text = this.responseText;
|
||||
} else if (this.responseType === 'json' && this.response) {
|
||||
try {
|
||||
text = JSON.stringify(this.response);
|
||||
} catch (e) {
|
||||
text = null;
|
||||
}
|
||||
}
|
||||
if (text) {
|
||||
console.log(LOG_PREFIX, 'Response received, length:', text.length);
|
||||
parseAndPost(text);
|
||||
} else {
|
||||
console.warn(LOG_PREFIX, 'Could not read response text, responseType:', this.responseType);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(LOG_PREFIX, 'Error reading response:', e);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// swallow
|
||||
}
|
||||
return originalSend.apply(this, arguments);
|
||||
};
|
||||
|
||||
// --- Monkey-patch fetch (au cas ou SmartUI l'utilise) ---
|
||||
if (typeof window.fetch === 'function') {
|
||||
const originalFetch = window.fetch;
|
||||
window.fetch = function (input, init) {
|
||||
let url = '';
|
||||
try {
|
||||
if (typeof input === 'string') {
|
||||
url = input;
|
||||
} else if (input && typeof input.url === 'string') {
|
||||
url = input.url;
|
||||
}
|
||||
} catch (e) {
|
||||
url = '';
|
||||
}
|
||||
|
||||
const promise = originalFetch.apply(this, arguments);
|
||||
|
||||
if (url && url.indexOf(TARGET_SUBSTR) !== -1) {
|
||||
promise
|
||||
.then((response) => {
|
||||
try {
|
||||
const clone = response.clone();
|
||||
clone
|
||||
.text()
|
||||
.then((text) => parseAndPost(text))
|
||||
.catch(() => {});
|
||||
} catch (e) {
|
||||
// swallow
|
||||
}
|
||||
return response;
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
return promise;
|
||||
};
|
||||
}
|
||||
})();
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 465 B |
Binary file not shown.
|
After Width: | Height: | Size: 825 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128">
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#3b82f6"/>
|
||||
<stop offset="100%" stop-color="#1d4ed8"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Fond arrondi -->
|
||||
<rect x="0" y="0" width="128" height="128" rx="24" ry="24" fill="url(#bg)"/>
|
||||
|
||||
<!-- Pince de presse-papier (haut) -->
|
||||
<rect x="48" y="14" width="32" height="14" rx="4" ry="4" fill="#ffffff" opacity="0.92"/>
|
||||
|
||||
<!-- Texte "ID" -->
|
||||
<text x="64" y="92"
|
||||
font-family="-apple-system, 'Segoe UI', Roboto, sans-serif"
|
||||
font-weight="700"
|
||||
font-size="64"
|
||||
text-anchor="middle"
|
||||
fill="#ffffff">ID</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 789 B |
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "TakeID - WMS SmartUI ID Grabber",
|
||||
"version": "1.0.0",
|
||||
"description": "Copie les IDs internes (GUIDs) des elements selectionnes dans les vues SmartUI du WMS EasyWMS.",
|
||||
"permissions": ["activeTab", "clipboardWrite", "scripting"],
|
||||
"host_permissions": ["*://*/*"],
|
||||
"action": {
|
||||
"default_popup": "popup/popup.html",
|
||||
"default_title": "TakeID",
|
||||
"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",
|
||||
"all_frames": true
|
||||
}
|
||||
],
|
||||
"web_accessible_resources": [
|
||||
{
|
||||
"resources": ["content/injected.js"],
|
||||
"matches": ["*://*/*"]
|
||||
}
|
||||
],
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
+222
@@ -0,0 +1,222 @@
|
||||
:root {
|
||||
--bg: #ffffff;
|
||||
--fg: #1f2937;
|
||||
--muted: #6b7280;
|
||||
--primary: #2563eb;
|
||||
--primary-hover: #1d4ed8;
|
||||
--success: #16a34a;
|
||||
--warning: #d97706;
|
||||
--error: #dc2626;
|
||||
--border: #e5e7eb;
|
||||
--surface: #f9fafb;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
width: 360px;
|
||||
min-height: 260px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
font-size: 13px;
|
||||
color: var(--fg);
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 12px 14px 10px 14px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.header-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
|
||||
.view-name {
|
||||
display: block;
|
||||
font-size: 11.5px;
|
||||
color: var(--muted);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 250px;
|
||||
}
|
||||
|
||||
.view-name::before {
|
||||
content: "Vue : ";
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 24px;
|
||||
height: 22px;
|
||||
padding: 0 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
background: var(--muted);
|
||||
border-radius: 999px;
|
||||
}
|
||||
.badge.badge--active {
|
||||
background: var(--primary);
|
||||
}
|
||||
|
||||
.status {
|
||||
margin: 10px 14px 6px 14px;
|
||||
padding: 8px 10px;
|
||||
font-size: 12px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid transparent;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.status--info {
|
||||
color: #1e3a8a;
|
||||
background: #eff6ff;
|
||||
border-color: #bfdbfe;
|
||||
}
|
||||
.status--ok {
|
||||
color: #166534;
|
||||
background: #f0fdf4;
|
||||
border-color: #bbf7d0;
|
||||
}
|
||||
.status--warn {
|
||||
color: #92400e;
|
||||
background: #fffbeb;
|
||||
border-color: #fde68a;
|
||||
}
|
||||
.status--error {
|
||||
color: #991b1b;
|
||||
background: #fef2f2;
|
||||
border-color: #fecaca;
|
||||
}
|
||||
|
||||
.preview-wrapper {
|
||||
padding: 4px 14px 0 14px;
|
||||
}
|
||||
|
||||
.preview-label {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--muted);
|
||||
margin: 6px 0 4px 0;
|
||||
}
|
||||
|
||||
.preview {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 4px;
|
||||
height: 140px;
|
||||
overflow: auto;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
font-size: 11.5px;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.preview-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 5px 8px;
|
||||
border-radius: 4px;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.preview-item + .preview-item {
|
||||
margin-top: 2px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.preview-code {
|
||||
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
||||
font-size: 11.5px;
|
||||
color: #111827;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.preview-id {
|
||||
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
||||
font-size: 10.5px;
|
||||
color: #6b7280;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.preview-more {
|
||||
padding: 6px 8px;
|
||||
font-size: 11px;
|
||||
font-style: italic;
|
||||
color: var(--muted);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: 10px 14px 14px 14px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
padding: 9px 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background-color 120ms ease, color 120ms ease, border-color 120ms ease;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.btn--primary {
|
||||
color: #fff;
|
||||
background: var(--primary);
|
||||
}
|
||||
.btn--primary:hover:not(:disabled) {
|
||||
background: var(--primary-hover);
|
||||
}
|
||||
|
||||
.btn--ok {
|
||||
color: #fff;
|
||||
background: var(--success);
|
||||
}
|
||||
|
||||
.btn--error {
|
||||
color: #fff;
|
||||
background: var(--error);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>TakeID</title>
|
||||
<link rel="stylesheet" href="popup.css" />
|
||||
</head>
|
||||
<body>
|
||||
<header class="header">
|
||||
<div class="header-text">
|
||||
<h1 class="title">TakeID - SmartUI</h1>
|
||||
<span id="view-name" class="view-name" hidden></span>
|
||||
</div>
|
||||
<span id="count-badge" class="badge">0</span>
|
||||
</header>
|
||||
|
||||
<section id="status" class="status status--info">Lecture de la page...</section>
|
||||
|
||||
<section class="preview-wrapper">
|
||||
<label class="preview-label">Sélection</label>
|
||||
<ul id="preview" class="preview" aria-live="polite"></ul>
|
||||
</section>
|
||||
|
||||
<footer class="footer">
|
||||
<button id="copy-btn" class="btn btn--primary" type="button" disabled>
|
||||
<span class="btn-label">Copier les IDs</span>
|
||||
</button>
|
||||
</footer>
|
||||
|
||||
<script src="popup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
+277
@@ -0,0 +1,277 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const browser = globalThis.browser || globalThis.chrome;
|
||||
|
||||
const statusEl = document.getElementById('status');
|
||||
const countBadge = document.getElementById('count-badge');
|
||||
const previewEl = document.getElementById('preview');
|
||||
const copyBtn = document.getElementById('copy-btn');
|
||||
const copyLabel = copyBtn.querySelector('.btn-label');
|
||||
const viewNameEl = document.getElementById('view-name');
|
||||
|
||||
let currentIds = [];
|
||||
|
||||
function parseViewName(url) {
|
||||
if (!url) return null;
|
||||
try {
|
||||
const u = new URL(url);
|
||||
const raw = u.searchParams.get('viewname');
|
||||
if (!raw) return null;
|
||||
// Format attendu : AppName|ViewName
|
||||
const parts = raw.split('|');
|
||||
const name = parts.length > 1 ? parts[1] : parts[0];
|
||||
return name ? name.trim() : null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function setViewName(name) {
|
||||
if (!name) {
|
||||
viewNameEl.hidden = true;
|
||||
viewNameEl.textContent = '';
|
||||
viewNameEl.removeAttribute('title');
|
||||
return;
|
||||
}
|
||||
viewNameEl.textContent = name;
|
||||
viewNameEl.title = name;
|
||||
viewNameEl.hidden = false;
|
||||
}
|
||||
|
||||
function setStatus(text, level) {
|
||||
statusEl.textContent = text;
|
||||
statusEl.className = 'status status--' + (level || 'info');
|
||||
}
|
||||
|
||||
function setBadge(n) {
|
||||
countBadge.textContent = String(n);
|
||||
countBadge.classList.toggle('badge--active', n > 0);
|
||||
}
|
||||
|
||||
function setPreview(pairs) {
|
||||
// Vide le contenu existant
|
||||
while (previewEl.firstChild) previewEl.removeChild(previewEl.firstChild);
|
||||
|
||||
if (!pairs || pairs.length === 0) return;
|
||||
|
||||
const MAX_PREVIEW = 50;
|
||||
const visible = pairs.slice(0, MAX_PREVIEW);
|
||||
|
||||
for (const pair of visible) {
|
||||
const item = document.createElement('li');
|
||||
item.className = 'preview-item';
|
||||
|
||||
const codeEl = document.createElement('span');
|
||||
codeEl.className = 'preview-code';
|
||||
codeEl.textContent = pair.code || '(code inconnu)';
|
||||
codeEl.title = codeEl.textContent;
|
||||
|
||||
const idEl = document.createElement('span');
|
||||
idEl.className = 'preview-id';
|
||||
idEl.textContent = pair.id;
|
||||
idEl.title = pair.id;
|
||||
|
||||
item.appendChild(codeEl);
|
||||
item.appendChild(idEl);
|
||||
previewEl.appendChild(item);
|
||||
}
|
||||
|
||||
if (pairs.length > MAX_PREVIEW) {
|
||||
const more = document.createElement('li');
|
||||
more.className = 'preview-more';
|
||||
more.textContent = '... et ' + (pairs.length - MAX_PREVIEW) + ' de plus';
|
||||
previewEl.appendChild(more);
|
||||
}
|
||||
}
|
||||
|
||||
function setButtonState(state) {
|
||||
copyBtn.classList.remove('btn--primary', 'btn--ok', 'btn--error');
|
||||
switch (state) {
|
||||
case 'ok':
|
||||
copyBtn.classList.add('btn--ok');
|
||||
copyLabel.textContent = 'Copié';
|
||||
break;
|
||||
case 'error':
|
||||
copyBtn.classList.add('btn--error');
|
||||
copyLabel.textContent = 'Échec de la copie';
|
||||
break;
|
||||
case 'idle':
|
||||
default:
|
||||
copyBtn.classList.add('btn--primary');
|
||||
copyLabel.textContent = 'Copier les IDs';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function isSmartUiUrl(url) {
|
||||
if (!url) return false;
|
||||
try {
|
||||
const u = new URL(url);
|
||||
return /\/SmartUI\//i.test(u.pathname);
|
||||
} catch (e) {
|
||||
return /\/SmartUI\//i.test(url);
|
||||
}
|
||||
}
|
||||
|
||||
function queryActiveTab() {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
browser.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
||||
resolve(tabs && tabs[0] ? tabs[0] : null);
|
||||
});
|
||||
} catch (e) {
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function sendMessage(tabId, msg) {
|
||||
return new Promise((resolve) => {
|
||||
let settled = false;
|
||||
const done = (value) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
resolve(value);
|
||||
};
|
||||
try {
|
||||
browser.tabs.sendMessage(tabId, msg, (response) => {
|
||||
const err = browser.runtime && browser.runtime.lastError;
|
||||
if (err) {
|
||||
done({ ok: false, error: err.message || String(err) });
|
||||
} else {
|
||||
done(response || { ok: false, error: 'empty_response' });
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
done({ ok: false, error: String(e && e.message ? e.message : e) });
|
||||
}
|
||||
setTimeout(() => done({ ok: false, error: 'timeout' }), 1500);
|
||||
});
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
const tab = await queryActiveTab();
|
||||
if (!tab) {
|
||||
setStatus("Impossible de lire l'onglet actif.", 'error');
|
||||
setBadge(0);
|
||||
setPreview([]);
|
||||
setViewName(null);
|
||||
copyBtn.disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isSmartUiUrl(tab.url)) {
|
||||
setStatus("Cette page n'est pas une vue SmartUI.", 'warn');
|
||||
setBadge(0);
|
||||
setPreview([]);
|
||||
setViewName(null);
|
||||
copyBtn.disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
setViewName(parseViewName(tab.url));
|
||||
|
||||
let resp = await sendMessage(tab.id, { type: 'GET_SELECTED_IDS' });
|
||||
|
||||
// Si le content script ne repond pas, l'injecter programmatiquement et reessayer
|
||||
if (!resp || resp.ok === false) {
|
||||
setStatus("Injection du content script...", 'info');
|
||||
try {
|
||||
await browser.scripting.executeScript({
|
||||
target: { tabId: tab.id, allFrames: true },
|
||||
files: ['content/content-script.js']
|
||||
});
|
||||
// Attendre que le script s'initialise
|
||||
await new Promise((r) => setTimeout(r, 800));
|
||||
resp = await sendMessage(tab.id, { type: 'GET_SELECTED_IDS' });
|
||||
} catch (e) {
|
||||
console.warn('[TakeID] Injection failed:', e);
|
||||
}
|
||||
}
|
||||
|
||||
if (!resp || resp.ok === false) {
|
||||
setStatus(
|
||||
"Le content script ne répond pas. Rechargez la page SmartUI puis réouvrez le popup.",
|
||||
'error'
|
||||
);
|
||||
setBadge(0);
|
||||
setPreview([]);
|
||||
copyBtn.disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const pairs = Array.isArray(resp.pairs)
|
||||
? resp.pairs
|
||||
: (Array.isArray(resp.ids) ? resp.ids.map((id) => ({ id, code: null })) : []);
|
||||
currentIds = pairs.map((p) => p.id);
|
||||
|
||||
setBadge(currentIds.length);
|
||||
setPreview(pairs);
|
||||
|
||||
if (currentIds.length === 0) {
|
||||
if (!resp.cacheSize) {
|
||||
setStatus(
|
||||
"Aucune donnée en cache. Naviguez sur la grille (ou rafraîchissez la vue) pour que l'extension capture les IDs.",
|
||||
'warn'
|
||||
);
|
||||
} else if (!resp.selectedCount) {
|
||||
setStatus(
|
||||
"Aucune ligne sélectionnée. Cochez les lignes dans la grille puis réouvrez le popup.",
|
||||
'warn'
|
||||
);
|
||||
} else {
|
||||
setStatus(
|
||||
"Lignes sélectionnées détectées mais aucun ID associé trouvé dans le cache.",
|
||||
'warn'
|
||||
);
|
||||
}
|
||||
copyBtn.disabled = true;
|
||||
} else {
|
||||
setStatus(
|
||||
currentIds.length + ' ID' + (currentIds.length > 1 ? 's' : '') + ' prêt' + (currentIds.length > 1 ? 's' : '') + ' à copier.',
|
||||
'ok'
|
||||
);
|
||||
copyBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function copyIds() {
|
||||
if (!currentIds || currentIds.length === 0) return;
|
||||
const text = currentIds.join('\n');
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
setButtonState('ok');
|
||||
setStatus(currentIds.length + ' ID(s) copié(s) dans le presse-papier.', 'ok');
|
||||
setTimeout(() => setButtonState('idle'), 1400);
|
||||
} catch (e) {
|
||||
// Fallback : execCommand
|
||||
try {
|
||||
const ta = document.createElement('textarea');
|
||||
ta.value = text;
|
||||
ta.style.position = 'fixed';
|
||||
ta.style.opacity = '0';
|
||||
document.body.appendChild(ta);
|
||||
ta.select();
|
||||
const ok = document.execCommand('copy');
|
||||
document.body.removeChild(ta);
|
||||
if (ok) {
|
||||
setButtonState('ok');
|
||||
setStatus(currentIds.length + ' ID(s) copié(s).', 'ok');
|
||||
setTimeout(() => setButtonState('idle'), 1400);
|
||||
} else {
|
||||
throw new Error('execCommand failed');
|
||||
}
|
||||
} catch (e2) {
|
||||
setButtonState('error');
|
||||
setStatus('Échec de la copie : ' + (e2.message || e2), 'error');
|
||||
setTimeout(() => setButtonState('idle'), 1800);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
copyBtn.addEventListener('click', copyIds);
|
||||
|
||||
document.addEventListener('DOMContentLoaded', refresh);
|
||||
if (document.readyState !== 'loading') refresh();
|
||||
})();
|
||||
Reference in New Issue
Block a user