Skip to content

Instantly share code, notes, and snippets.

@HelloWorld017
Last active March 4, 2024 03:48
Show Gist options
  • Save HelloWorld017/08ec3c9cc6541256737529c0ec2389b2 to your computer and use it in GitHub Desktop.
Save HelloWorld017/08ec3c9cc6541256737529c0ec2389b2 to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name Github Action Pinning
// @namespace https://gist.github.com/HelloWorld017/08ec3c9cc6541256737529c0ec2389b2
// @namespace http://tampermonkey.net/
// @version 0.3
// @description Pin your favorite actions workflow in GitHub
// @author nenw
// @match https://github.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=github.com
// @grant none
// ==/UserScript==
/* Very Simple UI Library */
const getReplaceState = (elem) => {
const elements = (elem instanceof DocumentFragment) ? [...elem.children] : [elem];
return { elements };
};
const replaceWith = (replaceState, withElem) => {
const nextReplaceState = getReplaceState(withElem);
if (replaceState.elements.length === 1) {
replaceState.elements[0].parentNode.replaceChild(withElem, replaceState.elements[0]);
return nextReplaceState;
}
replaceState.elements[0].parentNode.insertBefore(withElem, replaceState.elements[0]);
replaceState.elements.forEach(elem => elem.parentNode.removeChild(elem));
return nextReplaceState;
};
const createElement = (tag, props, ns = null) => {
if (typeof tag === 'function') {
return tag(props);
}
const elem = ns === null ? document.createElement(tag) : document.createElementNS(ns, tag);
Object.keys(props).forEach(propKey => {
if (propKey.startsWith('on:')) {
elem.addEventListener(propKey.slice(3), props[propKey]);
return;
}
if (propKey === 'children') {
elem.appendChild(props[propKey]);
return;
}
elem.setAttribute(propKey, String(props[propKey]));
});
return elem;
};
const styled = (tag, style) => (props) => {
const elem = createElement(tag, props);
Object.keys(style).forEach(styleKey => {
elem.style[styleKey] = style[styleKey];
});
return elem;
};
const text = (contents) => () => {
const node = document.createTextNode(contents);
return node;
};
const svg = (tag) => (props) =>
createElement(tag, props, 'http://www.w3.org/2000/svg');
const jsx = (elems) => {
const jsxInternal = (elems) => {
if (elems.length <= 0) {
return [];
}
const [tag] = elems;
elems = elems.slice(1);
const props = {};
const [passedProps] = elems;
if (typeof passedProps === 'object' && !Array.isArray(passedProps)) {
elems = elems.slice(1);
Object.assign(props, passedProps);
}
const [children] = elems;
if (Array.isArray(children)) {
elems = elems.slice(1);
const childElems = jsx(children);
props.children = childElems;
}
const self = createElement(tag, props);
return [
self,
...jsxInternal(elems)
];
};
const output = jsxInternal(elems);
if (output.length === 0) {
return document.createComment('#');
}
if (output.length === 1) {
return output[0];
}
const fragment = document.createDocumentFragment();
output.forEach(elem => {
fragment.appendChild(elem);
});
return fragment;
};
/* Pinning */
const actionPinning = () => {
if (document.querySelector('[data-is="__action_pinning__"]')) {
return;
}
const parent = document.querySelector('[aria-label="Actions Workflows"] :where(.ActionListWrap, .ActionList)');
if (!parent) {
return;
}
/*
* Action List
*/
const ActionListItem = ({ name, href }) => jsx([
'li', { class: `ActionList-item${window.location.pathname === href ? ' ActionList-item--navActive' : ''}`, 'data-key': href }, [
'a', { href, class: 'ActionList-content ActionList-content--visual16' }, [
'span', { class: 'ActionList-item-label ActionList-item-label--truncate' }, [
text(name)
]
]
]
]);
const [, orgName, repoName] = window.location.href.match(/^https:\/\/github.com\/([^/]+)\/([^/]+)\/actions/) ?? ['', '', ''];
const pinKey = `${orgName}_${repoName}`;
const storageKey = `__action_pinning__${pinKey}`;
const pinnedActions = new Map(JSON.parse(window.localStorage.getItem(storageKey) ?? '[]'));
const PinnedActionsList = ({ pinnedActions }) => jsx([
'li', { class: 'ActionList-sectionDivider' }, [
'nav-list-group', [
'div', [
'div', { class: 'ActionList-sectionDivider' }, [
'h3', { class: 'ActionList-sectionDivider-title' }, [
text('Pinned')
]
],
'ul', { role: 'list', class: 'ActionListWrap' }, [
...Array.from(pinnedActions.entries()).flatMap(([href, name]) => [
ActionListItem, { href, name }
])
]
]
]
],
'li', { role: 'presentation', 'aria-hidden': 'true', class: 'ActionList-sectionDivider', 'data-is': '__action_pinning__' },
]);
const pinnedActionsElem = PinnedActionsList({ pinnedActions });
let pinnedActionsElemState = getReplaceState(pinnedActionsElem);
parent.prepend(pinnedActionsElem);
const updateActions = (update) => {
update();
window.localStorage.setItem(storageKey, JSON.stringify(Array.from(pinnedActions.entries())));
// No reconciliation, just replace
pinnedActionsElemState = replaceWith(pinnedActionsElemState, PinnedActionsList({ pinnedActions }));
};
const pinAction = (href, name) =>
updateActions(() => {
pinnedActions.set(href, name);
});
const unpinAction = (href) =>
updateActions(() => {
pinnedActions.delete(href);
});
/*
* Pin
*/
const pageHeader = document.querySelector('.PageHeader');
if (!pageHeader) {
return;
}
const href = location.pathname;
const name = pageHeader.querySelector('.PageHeader-title span').innerText;
const IconPin = () => jsx([
svg('svg'), { viewBox: '0 0 16 16', width: 16, height: 16, class: 'octicon octicon-pin Button-visual' }, [
svg('path'), {
d: 'm11.294.984 3.722 3.722a1.75 1.75 0 0 1-.504 2.826l-1.327.613a3.089 3.089 0 0 0-1.707 2.084l-.584 2.454c-.317 1.332-1.972 1.8-2.94.832L5.75 11.311 1.78 15.28' +
'a.749.749 0 1 1-1.06-1.06l3.969-3.97-2.204-2.204c-.968-.968-.5-2.623.832-2.94l2.454-.584a3.08 3.08 0 0 0 2.084-1.707l.613-1.327a1.75 1.75 0 0 1 2.826-.504Z' +
'M6.283 9.723l2.732 2.731a.25.25 0 0 0 .42-.119l.584-2.454a4.586 4.586 0 0 1 2.537-3.098l1.328-.613a.25.25 0 0 0 .072-.404l-3.722-3.722a.25.25 0 0 0-.404.072' +
'l-.613 1.328a4.584 4.584 0 0 1-3.098 2.537l-2.454.584a.25.25 0 0 0-.119.42l2.731 2.732Z'
}
]
]);
const IconUnpin = () => jsx([
svg('svg'), { viewBox: '0 0 16 16', width: 16, height: 16, class: 'octicon octicon-pin-slash Button-visual' }, [
svg('path'), {
d: 'm1.655.595 13.75 13.75q.22.219.22.53 0 .311-.22.53-.219.22-.53.22-.311 0-.53-.22L.595 1.655q-.22-.219-.22-.53 0-.311.22-.53.219-.22.53-.22.311 0 .53.22ZM.72 14.22l4.5-4.5' +
'q.219-.22.53-.22.311 0 .53.22.22.219.22.53 0 .311-.22.53l-4.5 4.5q-.219.22-.53.22-.311 0-.53-.22-.22-.219-.22-.53 0-.311.22-.53Z'
},
svg('path'), {
d: 'm5.424 6.146-1.759.419q-.143.034-.183.175-.04.141.064.245l5.469 5.469q.104.104.245.064.141-.04.175-.183l.359-1.509q.072-.302.337-.465.264-.163.567-.091.302.072.465.337.162.264.09.567' +
'l-.359 1.509q-.238.999-1.226 1.278-.988.28-1.714-.446L2.485 8.046q-.726-.726-.446-1.714.279-.988 1.278-1.226l1.759-.419q.303-.072.567.091.265.163.337.465.072.302-.091.567-.163.264-.465.336Z' +
'M7.47 3.47q.155-.156.247-.355l.751-1.627Q8.851.659 9.75.498q.899-.16 1.544.486l3.722 3.722q.646.645.486 1.544-.161.899-.99 1.282l-1.627.751' +
'q-.199.092-.355.247-.219.22-.53.22-.311 0-.53-.22-.22-.219-.22-.53 0-.311.22-.53.344-.345.787-.549l1.627-.751q.118-.055.141-.183.023-.128-.069-.221' +
'l-3.722-3.722q-.092-.092-.221-.069-.128.023-.183.141l-.751 1.627q-.204.443-.549.787-.219.22-.53.22-.311 0-.53-.22-.22-.219-.22-.53 0-.311.22-.53Z'
}
]
]);
let toggleCurrentAction;
const HeaderIcon = ({ isPinned }) => jsx([
'button', {
class: 'Button Button--iconOnly Button--secondary Button--medium',
style: 'margin-left: 4px',
'on:click': () => toggleCurrentAction(!isPinned)
}, [
isPinned ? IconUnpin : IconPin
]
]);
const headerIconElem = HeaderIcon({ isPinned: pinnedActions.has(href) });
let headerIconElemState = getReplaceState(headerIconElem);
toggleCurrentAction = (nextIsPinned) => {
if (nextIsPinned) {
pinAction(href, name);
} else {
unpinAction(href);
}
headerIconElemState = replaceWith(headerIconElemState, HeaderIcon({ isPinned: nextIsPinned }));
};
pageHeader.querySelector('.PageHeader-titleBar').prepend(headerIconElem);
};
const originalPushState = history.pushState;
history.pushState = function () {
originalPushState.apply(this, arguments);
actionPinning();
};
const originalReplaceState = history.replaceState;
history.replaceState = function () {
originalReplaceState.apply(this, arguments);
actionPinning();
};
window.addEventListener("popstate", actionPinning);
actionPinning();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment