|
// ==UserScript== |
|
// @name Stimtrack Enhancer |
|
// @namespace https://gist.github.com/Stanislas-Poisson/ea033ad287fb90cf3dc3cdc4741dc855 |
|
// @version 1.6.0 |
|
// @description Track title, quick navigation, markdown export + criticity indicator for Stimtrack |
|
// @author Stanislas Poisson |
|
// @license MIT |
|
// @match https://stimtrack.stimdata.tools/* |
|
// @updateURL https://gist.githubusercontent.com/Stanislas-Poisson/ea033ad287fb90cf3dc3cdc4741dc855/raw/TamperMonkey-StimData-StimTrack.js |
|
// @downloadURL https://gist.githubusercontent.com/Stanislas-Poisson/ea033ad287fb90cf3dc3cdc4741dc855/raw/TamperMonkey-StimData-StimTrack.js |
|
// @homepageURL https://gist.github.com/Stanislas-Poisson/ea033ad287fb90cf3dc3cdc4741dc855 |
|
// @supportURL https://www.patreon.com/c/zairakai |
|
// @icon https://www.google.com/s2/favicons?sz=64&domain=stimdata.tools |
|
// @run-at document-end |
|
// @grant none |
|
// ==/UserScript== |
|
|
|
/** |
|
* Stimtrack Enhancer |
|
* ================ |
|
* Author: Stanislas Poisson |
|
* |
|
* Features: |
|
* - Custom sticky track header |
|
* - Criticity indicator (color-coded urgency dot + shadow) |
|
* - Type indicator (Bug / Evolution / TODO icon) |
|
* - Markdown export (track + dev sections + attachments) |
|
* - Quick navigation input (direct track ID jump) |
|
* - UI fixes (Bootstrap select width, layout stability) |
|
* |
|
* Support: |
|
* - Patreon: https://www.patreon.com/c/zairakai |
|
* - Twitch: https://www.twitch.tv/zairakai |
|
*/ |
|
|
|
(function () { |
|
'use strict'; |
|
|
|
const params = new URLSearchParams(window.location.search); |
|
const bugId = params.get('bug_id'); |
|
|
|
/** |
|
* CONFIG |
|
*/ |
|
const CRITICITY_CONFIG = { |
|
bloquant: { color: '#eb3b3b', label: 'Bloquant' }, |
|
majeur: { color: '#f1af3e', label: 'Majeur' }, |
|
normal: { color: '#2081c8', label: 'Normal' }, |
|
mineur: { color: '#7dbdec', label: 'Mineur' }, |
|
trackday: { color: '#bc54bd', label: 'Trackday' } |
|
}; |
|
|
|
const TYPE_CONFIG = { |
|
bug: { icon: '🐞' }, |
|
evolution: { icon: '🚀' }, |
|
todo: { icon: '📝' } |
|
}; |
|
|
|
/** |
|
* GLOBAL STYLE FIX |
|
*/ |
|
function injectStyles() { |
|
if (document.getElementById('tm-style-fix')) return; |
|
|
|
const style = document.createElement('style'); |
|
style.id = 'tm-style-fix'; |
|
|
|
style.innerHTML = ` |
|
.bootstrap-select .filter-option.pull-left { width: auto !important; } |
|
.project-navbar-select { width: auto !important; max-width: none !important; } |
|
|
|
.tm-flex-wrapper { |
|
display: flex; |
|
align-items: stretch; |
|
gap: 8px; |
|
width: 100%; |
|
} |
|
|
|
#quick-track-input { |
|
height: auto; |
|
align-self: stretch; |
|
width: 80px; |
|
} |
|
|
|
#custom-track-header-sticky { |
|
position: sticky; |
|
top: 85px; |
|
z-index: 1040; |
|
display: grid; |
|
grid-template-columns: 1fr auto; |
|
margin: 0 -15px 10px; |
|
box-shadow: 0 .5rem .5rem rgb(0 0 0 / 5%); |
|
border-radius: 0 0 1rem 1rem; |
|
overflow: hidden; |
|
} |
|
|
|
.cth-h2 { |
|
width: 100%; |
|
height: 46px; |
|
background-color: #095893; |
|
} |
|
|
|
.cth-h2 h2 { |
|
margin: 0; |
|
padding: 0; |
|
line-height: 46px; |
|
font-size: 18px; |
|
color: #fff; |
|
margin-left: 10px; |
|
} |
|
|
|
.cth-alert { |
|
height: 46px; |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
background-color: #f9b229; |
|
color: #fff; |
|
border-left: 3px solid; |
|
padding: 0 10px; |
|
} |
|
|
|
.cth-alert-btn { |
|
color: inherit; |
|
text-decoration: none; |
|
display: flex; |
|
align-items: center; |
|
} |
|
|
|
.cth-alert-btn .glyphicon { margin-right: 10px; font-size: 18px; } |
|
.cth-alert-btn span { font-size: 18px; } |
|
|
|
.cth-track-id, |
|
.cth-markdown { |
|
display: flex; |
|
align-items: center; |
|
background: #f5f5f5; |
|
border-top: 1px solid #e0e0e0; |
|
padding: 6px 10px; |
|
} |
|
|
|
.cth-markdown { justify-content: flex-end; } |
|
|
|
.cth-track-id span { |
|
font-weight: 600; |
|
color: #555; |
|
font-size: 0.9em; |
|
} |
|
|
|
.cth-dot { |
|
width: 10px; |
|
height: 10px; |
|
border-radius: 50%; |
|
margin-right: 8px; |
|
display: inline-block; |
|
} |
|
|
|
.cth-type { |
|
margin-right: 6px; |
|
font-size: 14px; |
|
} |
|
`; |
|
|
|
document.head.appendChild(style); |
|
} |
|
|
|
/** |
|
* HELPERS |
|
*/ |
|
function getFieldValue(labelText) { |
|
const groups = document.querySelectorAll('.form-group'); |
|
|
|
for (const group of groups) { |
|
const label = group.querySelector('label')?.textContent |
|
?.replace(/\u00a0/g, ' ') |
|
?.replace(':', '') |
|
?.trim() |
|
?.toLowerCase(); |
|
|
|
if (label === labelText.toLowerCase()) { |
|
return group.querySelector('.form-control-static')?.innerText.trim(); |
|
} |
|
} |
|
|
|
return null; |
|
} |
|
|
|
function extractSectionData(section) { |
|
const data = {}; |
|
|
|
section.querySelectorAll('.form-group').forEach(group => { |
|
const labelEl = group.querySelector('label'); |
|
const valueEl = group.querySelector('.form-control-static'); |
|
|
|
if (!labelEl || !valueEl) return; |
|
|
|
const label = labelEl.textContent |
|
.replace(/\u00a0/g, ' ') |
|
.replace(':', '') |
|
.trim(); |
|
|
|
const value = valueEl.innerText.trim(); |
|
if (!value) return; |
|
|
|
data[label] = value; |
|
}); |
|
|
|
return data; |
|
} |
|
|
|
function extractAttachments(section) { |
|
const files = []; |
|
|
|
section.querySelectorAll('.download-all a[href*="docs/"]').forEach(link => { |
|
const name = link.textContent.trim(); |
|
const url = link.href; |
|
|
|
if (name) files.push({ name, url }); |
|
}); |
|
|
|
return files; |
|
} |
|
|
|
function formatEntry(key, value) { |
|
if (!value.includes('\n')) return `- ${key}: ${value}\n`; |
|
|
|
return `- ${key}:\n${value.split('\n').filter(l => l.trim()).map(l => ` ${l.trim()}`).join('\n')}\n`; |
|
} |
|
|
|
function buildMarkdown(trackId, trackData, devData, attachments) { |
|
let md = `# Track #${trackId}\n\n`; |
|
|
|
md += `## Track\n\n`; |
|
Object.entries(trackData).forEach(([k, v]) => md += formatEntry(k, v)); |
|
|
|
md += `\n## Développement\n\n`; |
|
Object.entries(devData).forEach(([k, v]) => md += formatEntry(k, v)); |
|
|
|
if (attachments.length) { |
|
md += `\n## Annexe\n\n`; |
|
attachments.forEach(a => md += `- ${a.name}\n`); |
|
} |
|
|
|
return md; |
|
} |
|
|
|
/** |
|
* STICKY HEADER |
|
*/ |
|
function injectBadge() { |
|
const alertAnchor = document.querySelector('.btn-open-modal[data-form="send-alert"]'); |
|
if (!alertAnchor || document.querySelector('#custom-track-header-sticky')) return; |
|
|
|
const titleDiv = document.querySelector('.title-track-view-div'); |
|
if (!titleDiv) return; |
|
|
|
injectStyles(); |
|
|
|
const criticityRaw = getFieldValue('Criticité'); |
|
const typeRaw = getFieldValue('Type de demande'); |
|
|
|
const criticityKey = criticityRaw?.toLowerCase(); |
|
const typeKey = typeRaw?.toLowerCase(); |
|
|
|
const criticity = CRITICITY_CONFIG[criticityKey] ?? { color: '#999', label: criticityRaw ?? 'N/A' }; |
|
const type = TYPE_CONFIG[typeKey] ?? { icon: '❓' }; |
|
|
|
const h2Text = titleDiv.querySelector('h2')?.textContent.trim() ?? ''; |
|
|
|
const sticky = document.createElement('div'); |
|
sticky.id = 'custom-track-header-sticky'; |
|
|
|
sticky.innerHTML = ` |
|
<div class="cth-h2"><h2>${h2Text}</h2></div> |
|
<div class="cth-alert"> |
|
<a href="#" class="cth-alert-btn"> |
|
<i class="glyphicon glyphicon-warning-sign"></i><span>Alerter</span> |
|
</a> |
|
</div> |
|
<div class="cth-track-id"> |
|
<span class="cth-dot"></span> |
|
<span class="cth-type">${type.icon}</span> |
|
<span>Track #${bugId}</span> |
|
</div> |
|
<div class="cth-markdown"> |
|
<button id="copy-md-btn" class="btn btn-default btn-sm"> |
|
<i class="fa fa-copy"></i> Copier Markdown |
|
</button> |
|
</div> |
|
`; |
|
|
|
const container = document.querySelector('.container'); |
|
if (container) container.insertBefore(sticky, container.firstChild); |
|
|
|
titleDiv.style.display = 'none'; |
|
alertAnchor.style.display = 'none'; |
|
|
|
sticky.querySelector('.cth-dot').style.backgroundColor = criticity.color; |
|
sticky.style.boxShadow = `0 .5rem .5rem ${criticity.color}55`; |
|
|
|
sticky.querySelector('.cth-alert-btn').addEventListener('click', e => { |
|
e.preventDefault(); |
|
alertAnchor.click(); |
|
}); |
|
|
|
const mdBtn = sticky.querySelector('#copy-md-btn'); |
|
|
|
mdBtn.addEventListener('click', () => { |
|
const trackSection = document.querySelector('#form_opea'); |
|
const devSection = document.querySelector('#form_dev'); |
|
|
|
if (!trackSection || !devSection) return; |
|
|
|
const md = buildMarkdown( |
|
bugId, |
|
extractSectionData(trackSection), |
|
extractSectionData(devSection), |
|
extractAttachments(trackSection) |
|
); |
|
|
|
navigator.clipboard.writeText(md); |
|
mdBtn.innerHTML = '<i class="fa fa-check"></i> Copié !'; |
|
|
|
setTimeout(() => { |
|
mdBtn.innerHTML = '<i class="fa fa-copy"></i> Copier Markdown'; |
|
}, 1500); |
|
}); |
|
} |
|
|
|
/** |
|
* QUICK NAV |
|
*/ |
|
function injectQuickNav() { |
|
const nav = document.querySelector('.project-navbar-select'); |
|
if (!nav || document.querySelector('#quick-track-input')) return; |
|
|
|
injectStyles(); |
|
|
|
const wrapper = document.createElement('div'); |
|
wrapper.className = 'tm-flex-wrapper'; |
|
|
|
const input = document.createElement('input'); |
|
input.id = 'quick-track-input'; |
|
input.type = 'number'; |
|
input.placeholder = 'Track ID'; |
|
input.className = 'form-control input-sm'; |
|
|
|
if (bugId) input.value = bugId; |
|
|
|
input.addEventListener('keydown', e => { |
|
if (e.key !== 'Enter') return; |
|
|
|
const id = input.value.trim(); |
|
if (!id) return; |
|
|
|
window.location.href = `https://stimtrack.stimdata.tools/bugs_voir.php?bug_id=${id}`; |
|
}); |
|
|
|
while (nav.firstChild) wrapper.appendChild(nav.firstChild); |
|
|
|
wrapper.insertBefore(input, wrapper.firstChild); |
|
nav.appendChild(wrapper); |
|
} |
|
|
|
function init() { |
|
if (bugId) injectBadge(); |
|
injectQuickNav(); |
|
} |
|
|
|
const observer = new MutationObserver(init); |
|
observer.observe(document.body, { childList: true, subtree: true }); |
|
|
|
if (document.readyState === 'loading') { |
|
document.addEventListener('DOMContentLoaded', init); |
|
} else { |
|
init(); |
|
} |
|
})(); |