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