diff options
author | 2021-04-08 10:45:57 +0200 | |
---|---|---|
committer | 2021-04-08 15:45:57 +0700 | |
commit | 0ac8f8ceec6127788a233cbb41c8ee7c459ffaa7 (patch) | |
tree | f850d33f3ea19bb2effee27a9456746b34031b6e | |
parent | 7882e292ee0eec130b542e017d918ab7f06555be (diff) | |
download | refined-github-0ac8f8ceec6127788a233cbb41c8ee7c459ffaa7.tar.gz refined-github-0ac8f8ceec6127788a233cbb41c8ee7c459ffaa7.tar.zst refined-github-0ac8f8ceec6127788a233cbb41c8ee7c459ffaa7.zip |
Help users find a feature with an interactive binary search (#4119)
Co-authored-by: Federico <me@fregante.com>
-rw-r--r-- | distribution/options.html | 16 | ||||
-rw-r--r-- | source/features/index.tsx | 20 | ||||
-rw-r--r-- | source/helpers/bisect.tsx | 105 | ||||
-rw-r--r-- | source/options.tsx | 21 | ||||
-rw-r--r-- | source/refined-github.css | 11 |
5 files changed, 161 insertions, 12 deletions
diff --git a/distribution/options.html b/distribution/options.html index ffaeb8ef..bd6c0602 100644 --- a/distribution/options.html +++ b/distribution/options.html @@ -60,9 +60,19 @@ </p> <p> - <label> - <button id="clear-cache">Clear cache</button> - </label> + <button id="clear-cache">Clear cache</button> + </p> + <p> + <strong>Find a feature</strong> + </p> + <p> + This process will help you identify what Refined GitHub feature is making changes or causing issues on GitHub. + </p> + <p id="find-feature-message" hidden> + Visit the GitHub page where you want to find the feature and refresh it to see the instructions. You can navigate to any page, but don’t use multiple tabs. + </p> + <p> + <button id="find-feature">Find feature…</button> </p> </div> diff --git a/source/features/index.tsx b/source/features/index.tsx index 512eb18a..139e09f3 100644 --- a/source/features/index.tsx +++ b/source/features/index.tsx @@ -9,6 +9,7 @@ import compareVersions from 'tiny-version-compare'; import * as pageDetect from 'github-url-detection'; import onNewComments from '../github-events/on-new-comments'; +import bisectFeatures from '../helpers/bisect'; import optionsStorage, {RGHOptions} from '../options-storage'; type BooleanFunction = () => boolean; @@ -93,12 +94,19 @@ const globalReady: Promise<RGHOptions> = new Promise(async resolve => { document.documentElement.classList.add('refined-github'); // Options defaults - const options = await optionsStorage.getAll(); - const hotfix = browser.runtime.getManifest().version === '0.0.0' || await cache.get('hotfix'); // Ignores the cache when loaded locally - - // If features are remotely marked as "seriously breaking" by the maintainers, disable them without having to wait for proper updates to propagate #3529 - void checkForHotfixes(); - Object.assign(options, hotfix); + const [options, hotfix, bisectedFeatures] = await Promise.all([ + optionsStorage.getAll(), + browser.runtime.getManifest().version === '0.0.0' || await cache.get('hotfix'), // Ignores the cache when loaded locally + bisectFeatures() + ]); + + if (bisectedFeatures) { + Object.assign(options, bisectedFeatures); + } else { + // If features are remotely marked as "seriously breaking" by the maintainers, disable them without having to wait for proper updates to propagate #3529 + void checkForHotfixes(); + Object.assign(options, hotfix); + } if (options.customCSS.trim().length > 0) { document.head.append(<style>{options.customCSS}</style>); diff --git a/source/helpers/bisect.tsx b/source/helpers/bisect.tsx new file mode 100644 index 00000000..7668e688 --- /dev/null +++ b/source/helpers/bisect.tsx @@ -0,0 +1,105 @@ +import React from 'dom-chef'; +import cache from 'webext-storage-cache'; +import select from 'select-dom'; + +import features from '../features'; +import pluralize from './pluralize'; + +// Split current list of features in half and create an options-like object to be applied on load +// Bisecting 4 features: enable 2 +// Bisecting 3 features: enable 1 +// Bisecting 2 features: enable 1 +// Bisecting 1 feature: enable 0 // This is the last step, if the user says Yes, it's not caused by a JS feature +const getMiddleStep = (list: any[]): number => Math.floor(list.length / 2); + +async function onChoiceButtonClick({currentTarget: button}: React.MouseEvent<HTMLButtonElement>): Promise<void> { + const answer = button.value; + const bisectedFeatures = (await cache.get<FeatureID[]>('bisect'))!; + + if (bisectedFeatures.length > 1) { + await cache.set('bisect', answer === 'yes' ? + bisectedFeatures.slice(0, getMiddleStep(bisectedFeatures)) : + bisectedFeatures.slice(getMiddleStep(bisectedFeatures)) + ); + + button.parentElement!.replaceWith(<div className="btn" aria-disabled="true">Reloading…</div>); + location.reload(); + return; + } + + // Last step, no JS feature was enabled + if (answer === 'yes') { + createMessageBox('No features were enabled on this page. Try disabling Refined GitHub to see if it belongs to it at all.'); + } else { + const feature = ( + <a href={'https://github.com/sindresorhus/refined-github/blob/main/source/features/' + bisectedFeatures[0] + '.tsx'}> + <code>{bisectedFeatures[0]}</code> + </a> + ); + + createMessageBox(<>The change or issue is caused by {feature}.</>); + } + + await cache.delete('bisect'); +} + +async function onEndButtonClick(): Promise<void> { + await cache.delete('bisect'); + location.reload(); +} + +function createMessageBox(message: Element | string, extraButtons?: Element): void { + select('#rgh-bisect-dialog')?.remove(); + document.body.append( + <div id="rgh-bisect-dialog" className="Box p-3"> + <p>{message}</p> + <div className="d-flex flex-justify-between"> + <button type="button" className="btn" onClick={onEndButtonClick}>Exit</button> + {extraButtons} + </div> + </div> + ); +} + +export default async function bisectFeatures(): Promise<Record<string, boolean> | void> { + // `bisect` stores the list of features to be split in half + const bisectedFeatures = await cache.get<FeatureID[]>('bisect'); + if (!bisectedFeatures) { + return; + } + + console.log(`Bisecting ${bisectedFeatures.length} features:\n${bisectedFeatures.join('\n')}`); + + const steps = Math.ceil(Math.log2(Math.max(bisectedFeatures.length))) + 1; + createMessageBox( + `Do you see the change or issue? (${pluralize(steps, 'last step', '$$ steps remaining')})`, + <div> + <button type="button" className="btn btn-danger mr-2" value="no" aria-disabled="true" onClick={onChoiceButtonClick}>No</button> + <button type="button" className="btn btn-primary" value="yes" aria-disabled="true" onClick={onChoiceButtonClick}>Yes</button> + </div> + ); + + // Enable "Yes"/"No" buttons once the page is done loading + window.addEventListener('load', () => { + for (const button of select.all('#rgh-bisect-dialog [aria-disabled]')) { + button.removeAttribute('aria-disabled'); + } + }); + + // Hide message when the process is done elsewhere + window.addEventListener('visibilitychange', async () => { + if (!await cache.get<FeatureID[]>('bisect')) { + createMessageBox('Process completed in another tab'); + } + }); + + const half = getMiddleStep(bisectedFeatures); + const temporaryOptions: Record<string, boolean> = {}; + for (const feature of features.list) { + const index = bisectedFeatures.indexOf(feature); + temporaryOptions[`feature:${feature}`] = index > -1 && index < half; + } + + console.log(temporaryOptions); + return temporaryOptions; +} diff --git a/source/options.tsx b/source/options.tsx index 87d412cb..27414276 100644 --- a/source/options.tsx +++ b/source/options.tsx @@ -16,6 +16,9 @@ interface Status { scopes?: string[]; } +// Don't repeat the magic variable, or its content will be inlined multiple times +const features = __featuresMeta__; + function reportStatus({error, text, scopes}: Status): void { const tokenStatus = select('#validation')!; tokenStatus.textContent = text ?? ''; @@ -129,6 +132,18 @@ async function clearCacheHandler(event: Event): Promise<void> { }, 2000); } +async function findFeatureHandler(event: Event): Promise<void> { + await cache.set<FeatureID[]>('bisect', features.map(({id}) => id), {minutes: 5}); + + const button = event.target as HTMLButtonElement; + button.disabled = true; + setTimeout(() => { + button.disabled = false; + }, 10_000); + + select('#find-feature-message')!.hidden = false; +} + function featuresFilterHandler(event: Event): void { const keywords = (event.currentTarget as HTMLInputElement).value.toLowerCase() .replace(/\W/g, ' ') @@ -158,9 +173,6 @@ async function highlightNewFeatures(): Promise<void> { } async function generateDom(): Promise<void> { - // Don't repeat the magic variable, the content will be injected multiple times - const features = __featuresMeta__; - // Generate list select('.js-features')!.append(...features.map(buildFeatureCheckbox)); @@ -208,6 +220,9 @@ function addEventListeners(): void { // Add cache clearer select('#clear-cache')!.addEventListener('click', clearCacheHandler); + // Add bisect tool + select('#find-feature')!.addEventListener('click', findFeatureHandler); + // Add token validation select('[name="personalToken"]')!.addEventListener('input', validateToken); diff --git a/source/refined-github.css b/source/refined-github.css index e4361605..79ef5954 100644 --- a/source/refined-github.css +++ b/source/refined-github.css @@ -138,3 +138,14 @@ pr-branches .select-menu-item-text .diffstat { display: inline !important; } + +/* Style the "Find feature" dialog that appears on top of the page */ +#rgh-bisect-dialog { + position: fixed; + bottom: 50%; + right: 50%; + transform: translate(50%, 50%); + min-width: 380px; /* Avoids a width change on the last step which causes the YES button to be where NO was */ + box-shadow: var(--color-toast-shadow); + z-index: 2147483647; +} |