// Crée un objet global nommé fdi pour regrouper les fonctions liées à la recherche SharePoint ; bonne pratique : éviter de disperser les fonctions dans le scope global ; limite : un objet global peut être écrasé si un autre script utilise le même nom. let fdi = { // Stocke la dernière URL de requête exécutée ; utile pour le debug ou pour rejouer une requête ; limite : cette valeur est écrasée à chaque nouvelle recherche. lastQuery: null, // Stocke le dernier résultat brut renvoyé par SharePoint ; utile pour analyser la réponse complète ; limite : peut consommer de la mémoire si le résultat est volumineux. lastResult: null, // Déclare une fonction asynchrone de recherche SharePoint ; bonne pratique : utiliser async/await pour rendre le code plus lisible ; limite : nécessite un navigateur moderne. async search(siteUrl, query, targetId, selectProperties, rowLimit = 500, specialFields = [], startrow = 0) { // Construit l’URL de l’API Search SharePoint ; bonne pratique : centraliser la construction de la requête ; limite : une URL trop longue peut poser problème selon les navigateurs ou proxies. const requestUrl = // Ajoute l’endpoint REST de recherche SharePoint au site fourni ; bonne pratique : passer l’URL du site en paramètre pour rendre la fonction réutilisable ; limite : l’utilisateur doit avoir les droits sur ce site. `${siteUrl}/_api/search/query` + // Ajoute le texte de recherche KQL à la requête ; bonne pratique : encoder la valeur pour éviter les erreurs de caractères spéciaux ; limite : encodeURIComponent ne remplace pas une validation fonctionnelle de la requête KQL. `?querytext='${encodeURIComponent(query)}'` + // Ajoute la liste des propriétés managées à retourner ; bonne pratique : ne sélectionner que les champs nécessaires pour améliorer les performances ; limite : les propriétés doivent être bien déclarées comme managed properties dans SharePoint Search. `&selectproperties='${encodeURIComponent(selectProperties)}'` + // Ajoute un tri décroissant sur la date de dernière modification ; bonne pratique : présenter les contenus récents en premier ; limite : LastModifiedTime doit être disponible dans l’index de recherche. `&sortlist='Size:descending'` + // Limite le nombre de résultats retournés à 500 ; bonne pratique : éviter de remonter un volume illimité ; limite : 500 peut être trop élevé pour l’affichage direct dans un navigateur. `&rowlimit=${rowLimit}&startrow=${startrow}`; console.log(requestUrl); // Démarre un bloc try/catch pour gérer proprement les erreurs ; bonne pratique : toujours prévoir les erreurs réseau ou API ; limite : certaines erreurs SharePoint peuvent rester génériques côté client. try { // Affiche l’URL appelée dans la console pour faciliter le diagnostic ; bonne pratique : utile en développement ; limite : éviter de laisser trop de logs en production. console.log("Search URL :", requestUrl); // Exécute l’appel HTTP vers SharePoint avec fetch ; bonne pratique : préférer fetch à XMLHttpRequest pour un code moderne ; limite : fetch dépend du contexte d’authentification SharePoint courant. const response = await fetch(requestUrl, { // Définit la méthode HTTP utilisée ; bonne pratique : GET est adapté pour une requête de lecture ; limite : les URL GET peuvent devenir longues avec beaucoup de paramètres. method: "GET", // Définit les en-têtes HTTP envoyés à SharePoint ; bonne pratique : demander une réponse JSON moderne ; limite : SharePoint peut renvoyer une structure différente selon le mode OData. headers: { // Demande une réponse JSON sans métadonnées OData verboses ; bonne pratique : réduit la taille de la réponse ; limite : le parsing diffère de l’ancien mode odata=verbose. "Accept": "application/json;odata=nometadata" } }); // Vérifie si la réponse HTTP indique un succès ; bonne pratique : ne pas supposer qu’un fetch réussi signifie une réponse applicative valide ; limite : response.ok ne donne pas toujours le détail métier de l’erreur. if (!response.ok) { // Déclenche une erreur explicite avec le code HTTP ; bonne pratique : faciliter le debug ; limite : le message reste générique sans lecture détaillée du corps de réponse. throw new Error(`Erreur SharePoint : statut HTTP ${response.status}`); } // Convertit la réponse HTTP en objet JavaScript JSON ; bonne pratique : travailler ensuite sur un objet plutôt que du texte brut ; limite : échoue si SharePoint ne renvoie pas du JSON valide. const result = await response.json(); // Mémorise l’URL de la dernière requête exécutée ; bonne pratique : utile pour audit ou debug ; limite : ne constitue pas un historique complet des appels. this.lastQuery = requestUrl; // Mémorise le résultat brut de la dernière recherche ; bonne pratique : permet d’inspecter la réponse complète si besoin ; limite : peut contenir beaucoup d’informations inutiles pour l’affichage. this.lastResult = result; // Récupère la liste des lignes de résultats dans la réponse SharePoint ; bonne pratique : utiliser l’optional chaining pour éviter les erreurs si une propriété est absente ; limite : si la structure API change, rows sera vide. const rows = result.PrimaryQueryResult?.RelevantResults?.Table?.Rows || []; // Découpe la chaîne des propriétés sélectionnées en tableau de colonnes ; bonne pratique : travailler sur un tableau pour faciliter les traitements ; limite : suppose que les propriétés sont séparées par des virgules. const columns = selectProperties // Sépare les propriétés à partir des virgules ; bonne pratique : format simple et lisible ; limite : ne supporte pas les noms contenant eux-mêmes une virgule. .split(",") // Nettoie les espaces autour de chaque propriété ; bonne pratique : évite les erreurs dues à " Title" au lieu de "Title" ; limite : ne corrige pas les fautes de nom de propriété. .map(column => column.trim()); // Transforme chaque ligne SharePoint en objet JavaScript simple ; bonne pratique : rendre les données faciles à afficher ou exporter ; limite : les valeurs complexes restent sous forme de chaîne. let lastFileName = ""; let lastSize = 0; const data = rows.map((row, index) => { // Crée un objet représentant une ligne de résultat ; bonne pratique : construire un modèle clair pour l’affichage ; limite : cet objet ne contient que les colonnes demandées. const item = { // Ajoute le numéro de ligne dans le résultat local ; bonne pratique : facilite le repérage visuel ; limite : ce numéro n’est pas l’identifiant SharePoint. //rowNum: index, doublon: false }; // Prépare un dictionnaire temporaire des cellules SharePoint ; bonne pratique : évite une triple boucle coûteuse ; limite : si deux cellules ont la même clé, la dernière écrase la précédente. const cellMap = {}; // Parcourt les cellules renvoyées par SharePoint pour la ligne courante ; bonne pratique : convertir un tableau Key/Value en objet plus simple ; limite : suppose que row.Cells existe bien. row.Cells.forEach(cell => { // Ajoute la valeur de chaque cellule dans le dictionnaire en utilisant la clé SharePoint ; bonne pratique : accès rapide par nom de propriété ; limite : les valeurs nulles ou complexes ne sont pas transformées. if (isNaN(Number(cell.Value))) cellMap[cell.Key] = cell.Value; else { if (cell.Key === "Size") { let size = Number((Number(cell.Value) / 1024 / 1024).toFixed(2)); // Convertit la taille en Mo cellMap[cell.Key] = size; if (lastFileName === cellMap["Filename"] && lastSize === size) { item.doublon = true; } lastFileName = cellMap["Filename"]; lastSize = size; } else cellMap[cell.Key] = Number(cell.Value); } }); // Parcourt les colonnes demandées par l’utilisateur ; bonne pratique : garantir que chaque objet contient les mêmes propriétés ; limite : les colonnes inexistantes seront vides. columns.forEach(column => { // Alimente la propriété demandée avec la valeur trouvée ou une chaîne vide ; bonne pratique : évite les undefined dans l’affichage ; limite : une vraie valeur falsy comme 0 peut être remplacée par vide avec ||. item[column] = cellMap[column] || ""; }); // Retourne l’objet correspondant à une ligne transformée ; bonne pratique : garder la transformation pure et lisible ; limite : aucun enrichissement métier n’est effectué ici. return item; }); // Affiche les données sous forme de tableau dans la console ; bonne pratique : très lisible en debug ; limite : à éviter en production si beaucoup de données. console.table(data); // Appelle la fonction d’affichage HTML dans le div cible ; bonne pratique : séparer récupération des données et rendu visuel ; limite : le rendu HTML reste simple. this.renderResults(data, targetId, specialFields); // Affiche dans la console le nombre de résultats réellement retournés dans cette page ; bonne pratique : utile pour contrôler le volume affiché ; limite : RowCount n’est pas forcément le total global indexé. console.log("RowCount :", result.PrimaryQueryResult?.RelevantResults?.RowCount); // Affiche dans la console le nombre total estimé de résultats ; bonne pratique : utile pour savoir s’il faut paginer ; limite : TotalRows peut être une estimation selon SharePoint Search. console.log("TotalRows :", result.PrimaryQueryResult?.RelevantResults?.TotalRows); // Retourne les données transformées pour permettre une réutilisation externe ; bonne pratique : rend la fonction exploitable par un autre traitement ; limite : l’appelant doit attendre la Promise. return data; } // Capture les erreurs générées pendant l’appel ou le traitement ; bonne pratique : éviter une erreur JavaScript non gérée ; limite : ne distingue pas toujours les erreurs réseau, droits ou syntaxe KQL. catch (error) { // Affiche l’erreur complète dans la console ; bonne pratique : indispensable pour diagnostiquer ; limite : la console n’est pas visible par un utilisateur final. console.error("Erreur lors de la recherche SharePoint :", error); // Récupère le conteneur HTML cible ; bonne pratique : ne pas supposer que l’élément existe ; limite : si l’id est incorrect, aucun rendu ne sera possible. const container = document.getElementById(targetId); // Vérifie que le conteneur existe avant d’écrire dedans ; bonne pratique : évite une erreur JavaScript ; limite : l’utilisateur ne verra rien si l’id est faux. if (container) { // Affiche un message d’erreur dans le div cible ; bonne pratique : informer l’utilisateur final ; limite : attention à ne pas afficher d’informations sensibles en production. container.innerHTML = `
${this.escapeHtml(error.message)}
`; } } }, renderSpecialValue(value, field) { }, // Déclare une fonction responsable du rendu HTML des résultats ; bonne pratique : séparer la logique d’affichage de la logique de recherche ; limite : ce rendu ne gère pas encore pagination, tri ou filtre. renderResults(data, targetId, specialFields) { const container = document.getElementById(targetId); if (!container) { console.error("Conteneur introuvable :", targetId); return; } if (!Array.isArray(data) || data.length === 0) { container.innerHTML = '
Aucun résultat trouvé
'; return; } const headers = Object.keys(data[0]); const filters = {}; let sortColumn = null; let sortAscending = true; container.innerHTML = ""; const wrapper = document.createElement("div"); wrapper.style.backgroundColor = "#e6f4ff"; wrapper.style.border = "1px solid #b3d9ff"; wrapper.style.padding = "10px"; wrapper.style.maxHeight = "600px"; wrapper.style.overflow = "auto"; const table = document.createElement("table"); table.style.width = "100%"; table.style.borderCollapse = "collapse"; table.style.backgroundColor = "white"; //table.style.tableLayout = "fixed"; const thead = document.createElement("thead"); const headerRow = document.createElement("tr"); const tbody = document.createElement("tbody"); const compareValues = (valueA, valueB) => { const a = valueA ?? ""; const b = valueB ?? ""; const numberA = Number(a); const numberB = Number(b); const aIsNumber = String(a).trim() !== "" && Number.isFinite(numberA); const bIsNumber = String(b).trim() !== "" && Number.isFinite(numberB); if (aIsNumber && bIsNumber) { return numberA - numberB; } return String(a).localeCompare( String(b), "fr", { numeric: true, sensitivity: "base" } ); }; const renderBody = () => { let displayedData = data.filter(row => { return headers.every(header => { const selectedValue = filters[header]; if ( selectedValue === undefined || selectedValue === "" ) { return true; } return String(row[header] ?? "") === selectedValue; }); }); if (sortColumn !== null) { displayedData = displayedData.slice().sort((rowA, rowB) => { const result = compareValues( rowA[sortColumn], rowB[sortColumn] ); return sortAscending ? result : -result; }); } tbody.innerHTML = ""; if (displayedData.length === 0) { const emptyRow = document.createElement("tr"); const emptyCell = document.createElement("td"); emptyCell.colSpan = headers.length; emptyCell.textContent = "Aucun résultat ne correspond aux filtres"; emptyCell.style.padding = "15px"; emptyCell.style.textAlign = "center"; emptyRow.appendChild(emptyCell); tbody.appendChild(emptyRow); return; } let index = 0; displayedData.forEach(row => { index++; const tr = document.createElement("tr"); headers.forEach(header => { const td = document.createElement("td"); let spf = specialFields.find(f => f.Name === header); if (spf && spf.render) { td.appendChild(spf.render(row, spf)); td.title = row[header] ?? ""; } else { td.textContent = row[header] ?? ""; } td.style.border = "1px solid #cccccc"; td.style.padding = "6px"; td.style.whiteSpace = "nowrap"; td.style.backgroundColor = index % 2 === 0 ? "#ffffff" : "#f9f9f9"; tr.appendChild(td); }); tbody.appendChild(tr); }); }; headers.forEach(header => { const th = document.createElement("th"); th.style.border = "1px solid #cccccc"; th.style.padding = "6px"; th.style.backgroundColor = "#f2f2f2"; th.style.position = "sticky"; th.style.top = "0"; th.style.zIndex = "2"; th.style.minWidth = "130px"; //th.style.maxWidth = "4000px"; const sortButton = document.createElement("button"); sortButton.type = "button"; sortButton.textContent = header + " ⇅"; sortButton.style.width = "100%"; sortButton.style.border = "none"; sortButton.style.backgroundColor = "transparent"; sortButton.style.fontWeight = "bold"; sortButton.style.textAlign = "left"; sortButton.style.cursor = "pointer"; sortButton.style.padding = "4px"; sortButton.addEventListener("click", () => { if (sortColumn === header) { sortAscending = !sortAscending; } else { sortColumn = header; sortAscending = true; } headerRow .querySelectorAll("button") .forEach(button => { const columnName = button.dataset.column; button.textContent = columnName + " ⇅"; }); sortButton.textContent = header + (sortAscending ? " ▲" : " ▼"); renderBody(); }); sortButton.dataset.column = header; const select = document.createElement("select"); select.style.width = "100%"; select.style.marginTop = "5px"; select.style.padding = "4px"; const allOption = document.createElement("option"); allOption.value = ""; allOption.textContent = "Tous"; select.appendChild(allOption); const distinctValues = Array.from( new Set( data.map(row => String(row[header] ?? "")) ) ); distinctValues.sort(compareValues); distinctValues.forEach(value => { const option = document.createElement("option"); option.value = value; option.textContent = value === "" ? "(vide)" : value; select.appendChild(option); }); select.addEventListener("change", () => { filters[header] = select.value; renderBody(); }); th.appendChild(sortButton); th.appendChild(select); headerRow.appendChild(th); }); thead.appendChild(headerRow); table.appendChild(thead); table.appendChild(tbody); wrapper.appendChild(table); container.appendChild(wrapper); renderBody(); }, // Déclare une fonction utilitaire pour sécuriser l’affichage HTML ; bonne pratique : protéger contre les injections HTML/XSS ; limite : ne remplace pas une politique de sécurité complète côté serveur. escapeHtml(value) { // Convertit la valeur en chaîne et remplace les caractères HTML sensibles ; bonne pratique : toujours échapper les données venant de SharePoint ou d’un utilisateur ; limite : ne valide pas le contenu métier. return String(value) // Remplace les esperluettes pour éviter l’interprétation HTML ; bonne pratique : commencer par & pour éviter les doubles interprétations ; limite : aucune. .replace(/&/g, "&") // Remplace les chevrons ouvrants pour éviter les balises HTML injectées ; bonne pratique : protection XSS de base ; limite : ne filtre pas les URL dangereuses. .replace(//g, ">") // Remplace les guillemets doubles pour éviter les injections dans certains contextes HTML ; bonne pratique : utile si la valeur est réutilisée dans un attribut ; limite : ici les valeurs sont dans des cellules texte. .replace(/"/g, """) // Remplace les apostrophes pour éviter les injections dans certains attributs HTML ; bonne pratique : sécurisation complémentaire ; limite : ne gère pas tous les contextes comme JavaScript inline. .replace(/'/g, "'"); } }; const style = document.createElement("style"); style.textContent = ` .fdi-wrapper { background-color: #e6f4ff; border: 1px solid #b3d9ff; padding: 10px; max-height: 600px; overflow: auto; } .fdi-table { width: 100%; border-collapse: collapse; background-color: white; } .fdi-header { min-width: 130px; border: 1px solid #cccccc; padding: 6px; background-color: #f2f2f2; position: sticky; top: 0; z-index: 2; } .fdi-cell { border: 1px solid #cccccc; padding: 6px; white-space: nowrap; } .fdi-row-even { background-color: #ffffff; } .fdi-row-odd { background-color: #f9f9f9; } .fdi-duplicate { color: red; font-weight: bold; background-color: #ffe6e6; } .fdi-not-duplicate { color: green; } .fdi-link { color: #0067b8; text-decoration: none; } .fdi-link:hover { text-decoration: underline; } `; document.head.appendChild(style); const specialFields = [ { Name: "ParentLink", Type: "Link", DisplayValue: "Filename", render: (row) => { //console.log("specialFields", this); const a = document.createElement("a"); a.href = row.ParentLink; a.textContent = row.SiteTitle; return a; } }, { Name: "doublon", Type: "Boolean", DisplayValue: "Filename", render: (row, me) => { console.log("specialFields", this); const a = document.createElement("span"); a.textContent = row.doublon; a.style.color = row.doublon ? "red" : "green"; return a; } }, { Name: "LastModifiedTime", Type: "DateTime", DisplayValue: "LastModifiedTime", render: (row, me) => { const date = new Date(row[me.Name]); const yyyy = date.getFullYear(); const MM = String(date.getMonth() + 1).padStart(2, "0"); const dd = String(date.getDate()).padStart(2, "0"); const span = document.createElement("span"); span.textContent = `${yyyy}-${MM}-${dd}`; return span; } } ] // Définit la requête KQL utilisée pour chercher des éléments de type document/list item ; conseil : tester la requête dans SharePoint Search avant intégration ; limite : ContentTypeId:0x0100* peut remonter beaucoup d’éléments. const query = "ContentTypeId:0x0101*";//Path:https://sodetrel92400.sharepoint.com/sites/FR-DSB-ARCHIVES004_EXM AND // Définit les propriétés managées à récupérer depuis l’index de recherche ; conseil : limiter les champs aux besoins réels ; limite : ces propriétés doivent être disponibles dans le schéma de recherche. function launchQuery() { const req = document.getElementById("queryInput").value; const selectInput = document.getElementById("selectInput").value; const rowLimit = document.getElementById("rowLimitInput").value; const startRow = document.getElementById("startRowInput").value; fdi.search( _spPageContextInfo.webAbsoluteUrl, req, "fdiOutPut", selectInput, parseInt(rowLimit), specialFields, parseInt(startRow) ) .then(data => { console.log("Rows:", data.length); }) .catch(err => { console.error(err); }); } const selectProperties = "Filename,UIVersionStringOWSTEXT,Size,ParentLink,LastModifiedTime,ListItemID,ViewsLifeTime,ViewsLastMonths3,ViewsLifeTimeUniqueUsers,ViewerCount,SiteTitle"; document.body.innerHTML = `
Query: Select Properties: row limit: start row:
`; // Lance la recherche sur le site SharePoint cible ; conseil : vérifier que le contexte utilisateur a bien accès au site ; limite : l’appel fonctionnera seulement si la page est exécutée dans un contexte authentifié compatible. await fdi.search( _spPageContextInfo ? _spPageContextInfo.webAbsoluteUrl : "https://sodetrel92400.sharepoint.com/sites/ACTEB2BPartagedmarchecadrageMicrosoft", query, "fdiOutPut", selectProperties, 500,//rowlimit specialFields, 0//startrow ); const button = document.getElementById("searchButton"); button.textContent = "Run"; button.addEventListener( "click", launchQuery );