Last active
June 2, 2025 23:30
-
-
Save michaelbourne/e820b643152edf7ff6abd06be49d99a0 to your computer and use it in GitHub Desktop.
Commerce7 Tenants Table - TamperMonkey userscript to add a sortable table of your tenants on the Commerce7 platform
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name Commerce7 Tenants Table | |
// @namespace http://tampermonkey.net/ | |
// @version 1.0 | |
// @description Adds a sortable table of your tenants on the Commerce7 platform | |
// @author Michael Bourne | |
// @match https://*.admin.platform.commerce7.com/* | |
// @grant none | |
// ==/UserScript== | |
(function() { | |
'use strict'; | |
let tenantsData = null; | |
let isInitialized = false; | |
let xhrInterceptorSetup = false; | |
function getColorMode() { | |
return document.cookie.split('; ').find(row => row.startsWith('color-mode='))?.split('=')[1] || 'light'; | |
} | |
function injectStyles() { | |
if (document.querySelector('#c7-tenants-styles')) return; | |
const style = document.createElement('style'); | |
style.id = 'c7-tenants-styles'; | |
style.textContent = ` | |
.c7-tenants-table-container { | |
margin: 20px auto; | |
padding: 40px 50px 50px; | |
border-radius: 5px; | |
box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
max-width: 90%; | |
width: 575px; | |
font-size: 0.9rem; | |
} | |
.c7-tenants-table { | |
width: 100%; | |
border-collapse: collapse; | |
margin-top: 10px; | |
} | |
.c7-th { | |
padding: 12px; | |
text-align: left; | |
border-bottom: 1px solid; | |
cursor: pointer; | |
white-space: nowrap; | |
} | |
.c7-td { | |
padding: 12px; | |
border-bottom: 1px solid; | |
} | |
.c7-link { | |
text-decoration: none; | |
} | |
.c7-title { | |
margin: 0 0 20px 0; | |
font-size: 24px; | |
font-weight: 500; | |
} | |
[data-theme="light"].c7-tenants-table-container { | |
background: white; | |
} | |
[data-theme="light"] .c7-th { | |
background: #f8f9fa; | |
border-bottom-color: #ddd; | |
} | |
[data-theme="light"] .c7-td { | |
border-bottom-color: #ddd; | |
} | |
[data-theme="light"] .c7-link { | |
color: #007bff; | |
} | |
[data-theme="light"] .c7-title { | |
color: #333; | |
} | |
[data-theme="dark"].c7-tenants-table-container { | |
background-color: rgb(29, 35, 47); | |
} | |
[data-theme="dark"] .c7-th { | |
border-bottom-color: rgb(107, 114, 128); | |
background-color: rgb(22, 28, 39); | |
} | |
[data-theme="dark"] .c7-td { | |
border-bottom-color: rgb(107, 114, 128); | |
color: #e0e0e0; | |
} | |
[data-theme="dark"] .c7-link { | |
color: #4dabf7; | |
} | |
[data-theme="dark"] .c7-title { | |
color: #e0e0e0; | |
} | |
@media only screen and (max-width: 39.9375em) { | |
.c7-th-role, .c7-th-createdAt, | |
.c7-td-role, .c7-td-createdAt { | |
display: none; | |
} | |
.c7-tenants-table-container { | |
padding: 30px 25px 25px; | |
} | |
} | |
`; | |
document.head.appendChild(style); | |
} | |
function createTenantsTable(tenants) { | |
const container = document.createElement('div'); | |
container.className = 'c7-tenants-table-container'; | |
const table = document.createElement('table'); | |
table.className = 'c7-tenants-table'; | |
const thead = document.createElement('thead'); | |
const headerRow = document.createElement('tr'); | |
const headers = [ | |
{ text: 'Tenant ID', key: 'tenantId' }, | |
{ text: 'Tenant Name', key: 'tenant' }, | |
{ text: 'User Role', key: 'role' }, | |
{ text: 'Date Added', key: 'createdAt' } | |
]; | |
headers.forEach(header => { | |
const th = document.createElement('th'); | |
th.textContent = header.text; | |
th.dataset.sortKey = header.key; | |
th.className = `c7-th c7-th-${header.key}`; | |
th.addEventListener('click', () => sortTable(header.key)); | |
headerRow.appendChild(th); | |
}); | |
thead.appendChild(headerRow); | |
table.appendChild(thead); | |
const tbody = document.createElement('tbody'); | |
tenants.forEach(tenant => { | |
const row = document.createElement('tr'); | |
const idCell = document.createElement('td'); | |
idCell.className = 'c7-td c7-td-tenantId'; | |
const idLink = document.createElement('a'); | |
idLink.href = `https://${tenant.tenantId}.admin.platform.commerce7.com/`; | |
idLink.textContent = tenant.tenantId; | |
idLink.className = 'c7-link c7-link-tenantId'; | |
idCell.appendChild(idLink); | |
const nameCell = document.createElement('td'); | |
nameCell.className = 'c7-td c7-td-tenant'; | |
nameCell.textContent = tenant.tenant; | |
const roleCell = document.createElement('td'); | |
roleCell.className = 'c7-td c7-td-role'; | |
roleCell.textContent = tenant.role; | |
const dateCell = document.createElement('td'); | |
dateCell.className = 'c7-td c7-td-createdAt'; | |
dateCell.textContent = new Date(tenant.createdAt).toLocaleDateString(); | |
row.appendChild(idCell); | |
row.appendChild(nameCell); | |
row.appendChild(roleCell); | |
row.appendChild(dateCell); | |
tbody.appendChild(row); | |
}); | |
table.appendChild(tbody); | |
container.appendChild(table); | |
const title = document.createElement('h2'); | |
title.textContent = `Your Tenants (${tenants.length})`; | |
title.className = 'c7-title'; | |
container.insertBefore(title, table); | |
container.setAttribute('data-theme', getColorMode()); | |
return container; | |
} | |
function sortTable(key) { | |
const table = document.querySelector('.c7-tenants-table'); | |
const tbody = table.querySelector('tbody'); | |
const rows = Array.from(tbody.querySelectorAll('tr')); | |
const th = table.querySelector(`th[data-sort-key="${key}"]`); | |
const currentDirection = th.dataset.sortDirection || 'asc'; | |
const newDirection = currentDirection === 'asc' ? 'desc' : 'asc'; | |
th.dataset.sortDirection = newDirection; | |
const allThs = table.querySelectorAll('th'); | |
allThs.forEach(t => { | |
if (t !== th) { | |
delete t.dataset.sortDirection; | |
t.textContent = t.textContent.replace(' ↑', '').replace(' ↓', ''); | |
} | |
}); | |
th.textContent = th.textContent.replace(' ↑', '').replace(' ↓', '') + (newDirection === 'asc' ? ' ↑' : ' ↓'); | |
const sortedRows = rows.sort((a, b) => { | |
const aValue = a.children[getColumnIndex(key)].textContent; | |
const bValue = b.children[getColumnIndex(key)].textContent; | |
let comparison; | |
if (key === 'createdAt') { | |
comparison = new Date(aValue) - new Date(bValue); | |
} else { | |
comparison = aValue.localeCompare(bValue); | |
} | |
return newDirection === 'asc' ? comparison : -comparison; | |
}); | |
tbody.innerHTML = ''; | |
sortedRows.forEach(row => tbody.appendChild(row)); | |
} | |
function getColumnIndex(key) { | |
const headers = { | |
'tenantId': 0, | |
'tenant': 1, | |
'role': 2, | |
'createdAt': 3 | |
}; | |
return headers[key]; | |
} | |
function handleTenantData(data) { | |
if (data && data.tenants) { | |
const existingTable = document.querySelector('.c7-tenants-table-container'); | |
if (existingTable) { | |
existingTable.remove(); | |
} | |
tenantsData = data.tenants; | |
setTimeout(() => attemptToCreateTable(), 100); | |
} | |
} | |
function isTenantPage() { | |
const path = window.location.pathname; | |
return path.endsWith('/tenant') || path.includes('/tenant/'); | |
} | |
function attemptToCreateTable() { | |
if (!isTenantPage() || !tenantsData) return; | |
const targetElement = document.querySelector('#root > div'); | |
if (!targetElement) return; | |
const existingTable = document.querySelector('.c7-tenants-table-container'); | |
if (!existingTable) { | |
const table = createTenantsTable(tenantsData); | |
targetElement.appendChild(table); | |
} | |
} | |
function setupXHRInterceptor() { | |
if (xhrInterceptorSetup) return; | |
xhrInterceptorSetup = true; | |
const originalXHR = window.XMLHttpRequest; | |
window.XMLHttpRequest = function() { | |
const xhr = new originalXHR(); | |
const originalOpen = xhr.open; | |
const originalSend = xhr.send; | |
xhr.open = function() { | |
if (arguments[1] === 'https://global-api.commerce7.com/v1/account/self') { | |
this._isTargetRequest = true; | |
arguments[1] = arguments[1] + '?_=' + Date.now(); | |
} | |
return originalOpen.apply(this, arguments); | |
}; | |
xhr.send = function() { | |
if (this._isTargetRequest) { | |
const originalOnReadyStateChange = this.onreadystatechange; | |
this.onreadystatechange = function() { | |
if (this.readyState === 4) { | |
try { | |
const data = JSON.parse(this.responseText); | |
handleTenantData(data); | |
} catch (error) {} | |
} | |
if (originalOnReadyStateChange) { | |
originalOnReadyStateChange.apply(this, arguments); | |
} | |
}; | |
} | |
return originalSend.apply(this, arguments); | |
}; | |
return xhr; | |
}; | |
} | |
function initialize() { | |
if (isInitialized) return; | |
isInitialized = true; | |
setupXHRInterceptor(); | |
injectStyles(); | |
const observer = new MutationObserver(() => { | |
if (isTenantPage()) { | |
attemptToCreateTable(); | |
} | |
}); | |
observer.observe(document.body, { | |
childList: true, | |
subtree: true | |
}); | |
attemptToCreateTable(); | |
} | |
function setupUrlMonitoring() { | |
const originalPushState = history.pushState; | |
history.pushState = function() { | |
originalPushState.apply(this, arguments); | |
if (isTenantPage()) { | |
attemptToCreateTable(); | |
} | |
}; | |
const originalReplaceState = history.replaceState; | |
history.replaceState = function() { | |
originalReplaceState.apply(this, arguments); | |
if (isTenantPage()) { | |
attemptToCreateTable(); | |
} | |
}; | |
window.addEventListener('popstate', function() { | |
if (isTenantPage()) { | |
attemptToCreateTable(); | |
} | |
}); | |
} | |
initialize(); | |
setupUrlMonitoring(); | |
if (document.readyState === 'loading') { | |
document.addEventListener('DOMContentLoaded', () => { | |
initialize(); | |
setupUrlMonitoring(); | |
}); | |
} | |
window.addEventListener('load', () => { | |
initialize(); | |
setupUrlMonitoring(); | |
}); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Updated to work on tenant id containing URLs (Switch Tenant screen)