diff options
Diffstat (limited to 'source/features/quick-fork-deletion.tsx')
-rw-r--r-- | source/features/quick-fork-deletion.tsx | 138 |
1 files changed, 138 insertions, 0 deletions
diff --git a/source/features/quick-fork-deletion.tsx b/source/features/quick-fork-deletion.tsx new file mode 100644 index 00000000..fef675b6 --- /dev/null +++ b/source/features/quick-fork-deletion.tsx @@ -0,0 +1,138 @@ +import './quick-fork-deletion.css'; +import delay from 'delay'; +import React from 'dom-chef'; +import select from 'select-dom'; +import delegate from 'delegate-it'; +import elementReady from 'element-ready'; +import * as pageDetect from 'github-url-detection'; + +import features from '.'; +import * as api from '../github-helpers/api'; +import {getRepo} from '../github-helpers'; +import pluralize from '../helpers/pluralize'; +import addNotice from '../github-widgets/notice-bar'; +import looseParseInt from '../helpers/loose-parse-int'; +import parseBackticks from '../github-helpers/parse-backticks'; + +function handleToggle(event: delegate.Event<Event, HTMLDetailsElement>): void { + const hasContent = select.exists([ + '[data-hotkey="g i"] .Counter:not([hidden])', // Open issues + '[data-hotkey="g p"] .Counter:not([hidden])', // Open PRs + '.rgh-open-prs-of-forks' // PRs opened in the source repo + ]); + + if (hasContent && !confirm('This fork has open issues/PRs, are you sure you want to delete everything?')) { + // Close the <details> element again + event.delegateTarget.open = false; + } else { + // Without the timeout, the same toggle event will also trigger the AbortController + setTimeout(start, 1, event.delegateTarget); + } +} + +async function buttonTimeout(buttonContainer: HTMLDetailsElement): Promise<boolean> { + // Watch for cancellations + const abortController = new AbortController(); + buttonContainer.addEventListener('toggle', () => { + abortController.abort(); + }, {once: true}); + + void api.expectTokenScope('delete_repo').catch((error: Error) => { + abortController.abort(); + buttonContainer.open = false; + addNotice([ + 'Could not delete the repository. ', + parseBackticks(error.message) + ], { + type: 'error', + action: ( + <a className="btn btn-sm primary flash-action" href="https://github.com/settings/tokens"> + Update token… + </a> + ) + }); + }); + + let secondsLeft = 5; + const button = select('.btn', buttonContainer)!; + try { + do { + button.style.transform = `scale(${1.2 - ((secondsLeft - 5) / 3)})`; // Dividend is zoom speed + button.textContent = `Deleting fork in ${pluralize(secondsLeft, '$$ second')}. Cancel?`; + await delay(1000, {signal: abortController.signal}); // eslint-disable-line no-await-in-loop + } while (--secondsLeft); + } catch { + button.textContent = 'Delete fork'; + button.style.transform = ''; + } + + return !abortController.signal.aborted; +} + +async function start(buttonContainer: HTMLDetailsElement): Promise<void> { + if (!await buttonTimeout(buttonContainer)) { + return; + } + + select('.btn', buttonContainer)!.textContent = 'Deleting fork…'; + + try { + const {nameWithOwner} = getRepo()!; + await api.v3('/repos/' + nameWithOwner, { + method: 'DELETE', + json: false + }); + addNotice(`Repository ${nameWithOwner} deleted`, {action: false}); + select('.application-main')!.remove(); + if (document.hidden) { + // Try closing the tab if in the background. Could fail, so we still update the UI above + void browser.runtime.sendMessage({closeTab: true}); + } + } catch (error: unknown) { + buttonContainer.closest('li')!.remove(); // Remove button + addNotice([ + 'Could not delete the repository. ', + (error as any).response?.message ?? (error as any).message + ], { + type: 'error' + }); + + throw error; + } +} + +async function init(): Promise<void | false> { + if ( + // Only if the user can delete the repository + !await elementReady('[data-tab-item="settings-tab"]') || + + // Only if the repository hasn't been starred + looseParseInt(select('.starring-container .social-count')!) > 0 + ) { + return false; + } + + await api.expectToken(); + + // (Ab)use the details element as state and an accessible "click-anywhere-to-cancel" utility + select('.pagehead-actions')!.prepend( + <li> + <details className="details-reset details-overlay select-menu rgh-quick-fork-deletion"> + <summary aria-haspopup="menu" role="button"> + {/* This extra element is needed to keep the button above the <summary>’s lightbox */} + <span className="btn btn-sm btn-danger">Delete fork</span> + </summary> + </details> + </li> + ); + + delegate(document, '.rgh-quick-fork-deletion[open]', 'toggle', handleToggle, true); +} + +void features.add(__filebasename, { + include: [ + pageDetect.isForkedRepo + ], + awaitDomReady: false, + init +}); |