(async function () {
async function getSPContext() {
const ctxAvailable = typeof _spPageContextInfo !== 'undefined' && _spPageContextInfo.webAbsoluteUrl;
let url = ctxAvailable ? _spPageContextInfo.webAbsoluteUrl : window.location.href.split('?')[0].split('#')[0];
if (!ctxAvailable) {
for (const s of ['/_layouts/','/_api/','/SitePages/','/Lists/','/Forms/','/Pages/','/SiteAssets/','/_app/']) {
const i = url.toLowerCase().indexOf(s.toLowerCase());
if (i > -1) { url = url.substring(0, i); break; }
}
url = url.replace(/\/[^\/]*\.aspx$/i, '');
}
const r = await fetch(`${url}/_api/contextinfo`, { method:'POST', headers:{'Accept':'application/json;odata=verbose'}, credentials:'same-origin' });
const c = (await r.json()).d.GetContextWebInformation;
return { webUrl: c.WebFullUrl, siteUrl: c.SiteFullUrl };
}
const ctx = await getSPContext();
const base = ctx.webUrl;
const G = async u => { const r = await fetch(u, { headers:{'Accept':'application/json'}, credentials:'same-origin' }); if (!r.ok) throw new Error(r.status + ' ' + (await r.text()).slice(0,140)); return r.json(); };
const esc = s => (s==null?'':String(s)).replace(/[&<>"]/g, c => ({'&':'&','<':'<','>':'>','"':'"'}[c]));
const fmtSize = n => n==null?'':n<1024?n+' B':n<1048576?(n/1024).toFixed(1)+' KB':(n/1048576).toFixed(1)+' MB';
const serverRel = (d, it) => { const root = decodeURIComponent(new URL(d.webUrl).pathname); const p = it.parentReference?.path||''; const rel = p.includes('root:') ? decodeURIComponent(p.split('root:')[1]||'') : ''; return root + rel + '/' + it.name; };
const site = await G(`${base}/_api/v2.1/sites/root`);
const siteId = site.id;
const drives = (await G(`${base}/_api/v2.1/drives?$top=200`)).value || [];
const lists = (await G(`${base}/_api/v2.1/sites/root/lists?$top=200`)).value || [];
const wrap = document.createElement('div');
wrap.style.cssText = 'position:fixed;top:15px;left:50%;transform:translateX(-50%);width:96%;max-width:1600px;height:85vh;background:#fff;border:2px solid #0078d4;z-index:10000;box-shadow:0 6px 24px rgba(0,0,0,.2);display:flex;flex-direction:column;font-family:Segoe UI,Arial,sans-serif;user-select:text';
wrap.innerHTML = `
<div style="display:flex;align-items:center;gap:12px;padding:10px 16px;background:#0078d4;color:#fff">
<strong style="font-size:15px">🌐 SharePoint Graph Explorer</strong>
<span style="opacity:.9">${esc(site.title||site.name||'')}</span>
<code style="background:rgba(255,255,255,.2);padding:2px 6px;border-radius:3px;font-size:11px">_api/v2.1</code>
<button id="gxClose" style="margin-left:auto;padding:6px 14px;background:#d83b01;color:#fff;border:none;cursor:pointer;border-radius:3px">Close</button>
</div>
<div style="flex:1;display:flex;overflow:hidden">
<div id="gxTree" style="width:44%;min-width:340px;overflow:auto;border-right:1px solid #ddd;padding:8px 4px;font-size:13px"></div>
<div id="gxDetail" style="flex:1;overflow:auto;padding:16px 20px;font-size:13px;color:#222"></div>
</div>`;
document.body.appendChild(wrap);
const treeEl = wrap.querySelector('#gxTree');
const right = wrap.querySelector('#gxDetail');
wrap.querySelector('#gxClose').onclick = () => wrap.remove();
let selectedRow = null;
const select = row => { if (selectedRow) selectedRow.style.background=''; selectedRow = row; row.style.background='#cfe4fa'; };
const BTN = 'padding:3px 10px;background:#107c10;color:#fff;border:none;border-radius:3px;cursor:pointer;font-size:11px;white-space:nowrap';
const copyBtn = text => { const b = document.createElement('button'); b.textContent='Copy'; b.style.cssText=BTN; b.onclick=()=>{ navigator.clipboard.writeText(text); b.textContent='Copied ✓'; setTimeout(()=>b.textContent='Copy',1200); }; return b; };
const header = (icon,name,kind) => { const d=document.createElement('div'); d.style.cssText='border-bottom:2px solid #0078d4;padding-bottom:8px;margin-bottom:12px'; d.innerHTML=`<div style="font-size:18px;font-weight:600">${icon} ${esc(name)}</div><div style="color:#888;font-size:12px;text-transform:uppercase;letter-spacing:.5px">${esc(kind)}</div>`; return d; };
const sect = t => { const d=document.createElement('div'); d.style.cssText='margin:16px 0 6px;font-size:12px;font-weight:700;color:#0078d4;text-transform:uppercase;letter-spacing:.5px'; d.textContent=t; return d; };
const metaGrid = pairs => { const d=document.createElement('div'); d.style.cssText='display:grid;grid-template-columns:auto 1fr;gap:4px 14px'; pairs.forEach(([k,v])=>{ if(v==null||v==='')return; const a=document.createElement('div'); a.style.cssText='color:#888'; a.textContent=k; const b=document.createElement('div'); b.style.wordBreak='break-all'; b.textContent=v; d.appendChild(a); d.appendChild(b); }); return d; };
const field = (label,value,hint) => { const d=document.createElement('div'); d.style.cssText='margin:8px 0'; const t=document.createElement('div'); t.style.cssText='font-size:11px;color:#666;margin-bottom:2px'; t.textContent=label+(hint?' — '+hint:''); const row=document.createElement('div'); row.style.cssText='display:flex;gap:8px;align-items:flex-start'; const code=document.createElement('code'); code.style.cssText='flex:1;word-break:break-all;background:#f6f8fa;padding:6px 8px;border-radius:4px;font-size:12px;border:1px solid #eaecef'; code.textContent=value; row.appendChild(code); row.appendChild(copyBtn(value)); d.appendChild(t); d.appendChild(row); return d; };
const detail = (...nodes) => { right.innerHTML=''; nodes.forEach(n => n && right.appendChild(n)); };
const rawJson = obj => {
const json = JSON.stringify(obj, null, 2);
const wrap = document.createElement('div'); wrap.style.cssText = 'margin-top:18px';
const bar = document.createElement('div'); bar.style.cssText = 'display:flex;gap:8px;align-items:center;margin-bottom:6px';
const title = document.createElement('span'); title.style.cssText = 'font-size:12px;font-weight:700;color:#0078d4;text-transform:uppercase;letter-spacing:.5px;margin-right:auto'; title.textContent = 'Raw Graph JSON';
const toggle = document.createElement('button'); toggle.style.cssText = 'padding:3px 10px;background:#0078d4;color:#fff;border:none;border-radius:3px;cursor:pointer;font-size:11px'; toggle.textContent = 'Show';
bar.appendChild(title); bar.appendChild(toggle); bar.appendChild(copyBtn(json));
const pre = document.createElement('pre'); pre.style.cssText = 'display:none;background:#1e1e1e;color:#d4d4d4;padding:12px;border-radius:6px;overflow:auto;max-height:440px;font-size:12px;line-height:1.5;white-space:pre;margin:0;tab-size:2';
pre.textContent = json;
toggle.onclick = () => { const show = pre.style.display === 'none'; pre.style.display = show ? 'block' : 'none'; toggle.textContent = show ? 'Hide' : 'Show'; };
wrap.appendChild(bar); wrap.appendChild(pre);
return wrap;
};
function showSite() {
detail(
header('🌐', site.title||site.name||'Site', 'Site'),
metaGrid([['URL',site.webUrl],['Template',site.template?.name],['Created',site.createdDateTime]]),
sect('IDs'),
field('Site ID (Graph composite)', siteId),
sect('Equivalent URLs'),
field('Graph (global)', `https://graph.microsoft.com/v1.0/sites/${siteId}`),
field('Graph (SharePoint-hosted)', `${base}/_api/v2.1/sites/root`),
field('SharePoint URL', site.webUrl),
field('REST API URL', `${base}/_api/web`),
rawJson(site)
);
}
function showDrive(d) {
const root = decodeURIComponent(new URL(d.webUrl).pathname);
detail(
header('🗂️', d.name, 'Document Library (drive)'),
metaGrid([['Type',d.driveType],['Modified',d.lastModifiedDateTime]]),
sect('IDs for Power Automate / Graph'),
field('Drive ID', d.id),
field('Send an HTTP request to SharePoint', `_api/v2.1/drives/${d.id}`, 'Uri'),
sect('Equivalent URLs'),
field('Graph (global)', `https://graph.microsoft.com/v1.0/drives/${d.id}`),
field('Graph (SharePoint-hosted)', `${base}/_api/v2.1/drives/${d.id}`),
field('SharePoint URL', d.webUrl),
field('REST API URL', `${base}/_api/web/getlist('${root}')`),
rawJson(d)
);
}
function showDriveItem(d, it) {
const driveId = it.parentReference?.driveId || d.id, id = it.id, isF = !!it.folder, sr = serverRel(d, it);
detail(
header(isF?'📁':'📄', it.name, isF?'Folder':'File'),
metaGrid([['Size', isF ? (it.folder.childCount+' items') : fmtSize(it.size)],['Modified',it.lastModifiedDateTime],['Modified by',it.lastModifiedBy?.user?.displayName],['MIME',it.file?.mimeType]]),
sect('IDs for Power Automate / Graph'),
field('Drive ID', driveId),
field('Item ID (DriveItem ID)', id),
field('Send an HTTP request to SharePoint', `_api/v2.1/drives/${driveId}/items/${id}`, 'Uri'),
sect('Equivalent URLs'),
field('Graph (global)', `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${id}`),
field('Graph (SharePoint-hosted)', `${base}/_api/v2.1/drives/${driveId}/items/${id}`),
field('SharePoint URL', it.webUrl),
field('REST API URL', `${base}/_api/web/${isF?'getfolderbyserverrelativeurl':'getfilebyserverrelativeurl'}('${sr}')`),
it['@content.downloadUrl'] ? field('Direct download URL', it['@content.downloadUrl']) : null,
rawJson(it)
);
}
function showList(l) {
detail(
header('📋', l.displayName||l.name, 'List · '+(l.list?.template||'')),
metaGrid([['Items',l.itemCount],['URL',l.webUrl]]),
sect('IDs'),
field('List ID', l.id),
sect('Equivalent URLs'),
field('Graph (global)', `https://graph.microsoft.com/v1.0/sites/${siteId}/lists/${l.id}`),
field('Graph (SharePoint-hosted)', `${base}/_api/v2.1/sites/root/lists/${l.id}`),
field('SharePoint URL', l.webUrl),
field('REST API URL', `${base}/_api/web/lists(guid'${l.id}')`),
rawJson(l)
);
}
function showListItem(l, it) {
const id = it.id, listId = it.parentReference?.listId || l.id, f = it.fields || {};
const shown = Object.keys(f).filter(k => !k.startsWith('@') && !k.startsWith('_')).slice(0,12).map(k => [k, typeof f[k]==='object' ? JSON.stringify(f[k]) : f[k]]);
detail(
header('📝', (f.Title||f.FileLeafRef||('Item '+id)), 'List item · '+(l.displayName||l.name)),
sect('Fields'), metaGrid(shown),
sect('IDs for Power Automate / Graph'),
field('List ID', listId),
field('Item ID', id),
field('Send an HTTP request to SharePoint', `_api/v2.1/sites/root/lists/${listId}/items/${id}`, 'Uri'),
sect('Equivalent URLs'),
field('Graph (global)', `https://graph.microsoft.com/v1.0/sites/${siteId}/lists/${listId}/items/${id}`),
field('Graph (SharePoint-hosted)', `${base}/_api/v2.1/sites/root/lists/${listId}/items/${id}`),
field('SharePoint URL', it.webUrl),
field('REST API URL', `${base}/_api/web/lists(guid'${listId}')/items(${id})`),
rawJson(it)
);
}
function createNode({ level, icon, label, meta, expandable, loadChildren, onSelect }) {
const w = document.createElement('div');
const row = document.createElement('div');
row.style.cssText = `display:flex;align-items:center;gap:4px;padding:3px 6px;cursor:pointer;border-radius:3px;padding-left:${6+level*16}px`;
row.onmouseenter = () => { if (row !== selectedRow) row.style.background='#f0f6fc'; };
row.onmouseleave = () => { if (row !== selectedRow) row.style.background=''; };
const tw = document.createElement('span'); tw.style.cssText='width:12px;flex:0 0 12px;color:#888;font-size:10px;text-align:center'; tw.textContent = expandable ? '▶' : '';
const lbl = document.createElement('span'); lbl.style.cssText='overflow:hidden;text-overflow:ellipsis;white-space:nowrap'; lbl.innerHTML = `${icon} ${esc(label)}` + (meta ? ` <span style="color:#999;font-size:11px">· ${esc(meta)}</span>` : '');
row.appendChild(tw); row.appendChild(lbl);
const kids = document.createElement('div'); kids.style.display = 'none';
w.appendChild(row); w.appendChild(kids);
let loaded = false, open = false;
row.addEventListener('click', async () => {
select(row); if (onSelect) onSelect();
if (!expandable) return;
open = !open; tw.textContent = open ? '▼' : '▶'; kids.style.display = open ? 'block' : 'none';
if (open && !loaded) {
loaded = true;
kids.innerHTML = `<div style="padding-left:${6+(level+1)*16}px;color:#0078d4;font-style:italic">Loading…</div>`;
try { await loadChildren(kids, level+1); }
catch (err) { kids.innerHTML = `<div style="color:#d83b01;padding-left:${6+(level+1)*16}px">${esc(err.message)}</div>`; }
}
});
return { w, kids, row };
}
function pager(container, level, firstUrl, renderItem) {
let next = firstUrl;
const more = document.createElement('div');
more.style.cssText = `padding:4px 6px;padding-left:${6+level*16}px;color:#0078d4;cursor:pointer;font-style:italic`;
more.textContent = '⋯ Load more';
const loadNext = async ev => {
if (ev && ev.stopPropagation) ev.stopPropagation();
more.textContent = 'Loading…';
try {
const data = await G(next);
(data.value || []).forEach(it => container.insertBefore(renderItem(it), more));
next = data['@odata.nextLink'] || null;
if (next) more.textContent = '⋯ Load more'; else more.remove();
} catch (err) { more.textContent = err.message; }
};
more.onclick = loadNext;
container.appendChild(more);
return loadNext;
}
function driveItemNode(level, d, it) {
const isF = !!it.folder;
return createNode({
level, icon: isF ? '📁' : '📄', label: it.name, meta: isF ? `${it.folder.childCount}` : fmtSize(it.size),
expandable: isF && it.folder.childCount > 0,
loadChildren: (kids, lv) => loadPaged(kids, lv, it2 => driveItemNode(lv, d, it2), `${base}/_api/v2.1/drives/${d.id}/items/${it.id}/children?$top=200&$expand=listItem`),
onSelect: () => showDriveItem(d, it)
}).w;
}
async function loadPaged(kids, level, render, firstUrl) {
kids.innerHTML = '';
const loadNext = pager(kids, level, firstUrl, render);
await loadNext();
}
async function loadDriveRoot(kids, level, d) {
kids.innerHTML = '';
const sRow = document.createElement('div'); sRow.style.cssText = `padding:4px 6px;padding-left:${6+level*16}px`;
const inp = document.createElement('input'); inp.placeholder = '🔍 search this library… (Enter)';
inp.style.cssText = 'padding:3px 6px;width:85%;font-size:12px;border:1px solid #ccc;border-radius:3px';
inp.onclick = e => e.stopPropagation();
sRow.appendChild(inp); kids.appendChild(sRow);
const listing = document.createElement('div'); kids.appendChild(listing);
const rootUrl = `${base}/_api/v2.1/drives/${d.id}/root/children?$top=200&$expand=listItem`;
const show = async url => { listing.innerHTML = ''; const loadNext = pager(listing, level, url, it => driveItemNode(level, d, it)); await loadNext(); };
await show(rootUrl);
inp.addEventListener('keydown', async e => {
if (e.key !== 'Enter') return;
const q = inp.value.trim();
if (!q) return show(rootUrl);
listing.innerHTML = `<div style="padding-left:${6+level*16}px;color:#0078d4;font-style:italic">Searching…</div>`;
try {
const data = await G(`${base}/_api/v2.1/drives/${d.id}/root/search(q='${encodeURIComponent(q)}')?$top=200`);
const v = data.value || []; listing.innerHTML = '';
if (!v.length) listing.innerHTML = `<div style="padding-left:${6+level*16}px;color:#666">No matches</div>`;
else v.forEach(it => listing.appendChild(driveItemNode(level, d, it)));
} catch (err) { listing.innerHTML = `<div style="padding-left:${6+level*16}px;color:#d83b01">Search unavailable: ${esc(err.message)}</div>`; }
});
}
function listItemNode(level, l, it) {
const f = it.fields || {}, title = f.Title || f.FileLeafRef || f.LinkTitle || ('Item ' + it.id);
return createNode({ level, icon:'📝', label:`#${it.id} ${title}`, expandable:false, onSelect:()=>showListItem(l, it) }).w;
}
const grp = t => { const d=document.createElement('div'); d.style.cssText='padding:8px 6px 3px;font-size:11px;font-weight:700;color:#888;text-transform:uppercase;letter-spacing:.5px'; d.textContent=t; return d; };
treeEl.appendChild(createNode({ level:0, icon:'🌐', label:site.title||site.name||'Site', meta:'site', expandable:false, onSelect:showSite }).w);
treeEl.appendChild(grp(`Libraries (${drives.length})`));
drives.forEach(d => treeEl.appendChild(createNode({ level:0, icon:'🗂️', label:d.name, meta:'library', expandable:true, loadChildren:(k,lv)=>loadDriveRoot(k,lv,d), onSelect:()=>showDrive(d) }).w));
treeEl.appendChild(grp(`Lists (${lists.length})`));
lists.forEach(l => treeEl.appendChild(createNode({ level:0, icon:'📋', label:l.displayName||l.name, meta:`${l.itemCount}`, expandable:l.itemCount>0, loadChildren:(k,lv)=>loadPaged(k,lv,it=>listItemNode(lv,l,it),`${base}/_api/v2.1/sites/root/lists/${l.id}/items?$expand=fields&$top=100`), onSelect:()=>showList(l) }).w));
showSite();
console.log('%cGraph Explorer ready','color:#0078d4;font-weight:bold');
console.table(drives.map(d => ({ name:d.name, driveId:d.id })));
})();