summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Florent <cheap.glitch@gmail.com> 2021-04-08 10:45:57 +0200
committerGravatar GitHub <noreply@github.com> 2021-04-08 15:45:57 +0700
commit0ac8f8ceec6127788a233cbb41c8ee7c459ffaa7 (patch)
treef850d33f3ea19bb2effee27a9456746b34031b6e
parent7882e292ee0eec130b542e017d918ab7f06555be (diff)
downloadrefined-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.html16
-rw-r--r--source/features/index.tsx20
-rw-r--r--source/helpers/bisect.tsx105
-rw-r--r--source/options.tsx21
-rw-r--r--source/refined-github.css11
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;
+}