import './quick-repo-deletion.css'; import delay from 'delay'; import React from 'dom-chef'; import select from 'select-dom'; import {TrashIcon} from '@primer/octicons-react'; import elementReady from 'element-ready'; import {assertError} from 'ts-extras'; import * as pageDetect from 'github-url-detection'; import delegate, {DelegateEvent} from 'delegate-it'; import features from '../feature-manager'; import * as api from '../github-helpers/api'; import {getForkedRepo, 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'; import attachElement from '../helpers/attach-element'; function handleToggle(event: DelegateEvent): 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 repo has open issues/PRs, are you sure you want to delete everything?')) { // Close the
element again event.delegateTarget.open = false; return; } if (!pageDetect.isForkedRepo() && !confirm('⚠️ This action cannot be undone. This will permanently delete the repository, wiki, issues, comments, packages, secrets, workflow runs, and remove all collaborator associations.')) { event.delegateTarget.open = false; return; } // Without the timeout, the same toggle event will also trigger the AbortController setTimeout(start, 1, event.delegateTarget); } async function verifyScopesWhileWaiting(abortController: AbortController): Promise { try { await api.expectTokenScope('delete_repo'); } catch (error) { assertError(error); abortController.abort(); await addNotice([ 'Could not delete the repository. ', parseBackticks(error.message), ], { type: 'error', action: ( Update token… ), }); } } async function buttonTimeout(buttonContainer: HTMLDetailsElement): Promise { const abortController = new AbortController(); // Add a global click listener to avoid potential future issues with z-index document.addEventListener('click', event => { event.preventDefault(); abortController.abort(); buttonContainer.open = false; }, {once: true}); void verifyScopesWhileWaiting(abortController); 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 repo 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 { if (!await buttonTimeout(buttonContainer)) { return; } select('.btn', buttonContainer)!.textContent = 'Deleting repo…'; const {nameWithOwner, owner} = getRepo()!; try { await api.v3('/repos/' + nameWithOwner, { method: 'DELETE', json: false, }); } catch (error) { assertError(error); buttonContainer.closest('li')!.remove(); // Remove button await addNotice([ 'Could not delete the repository. ', (error as api.RefinedGitHubAPIError).response?.message ?? error.message, ], { type: 'error', }); throw error; } const forkSource = '/' + getForkedRepo()!; const restoreURL = pageDetect.isOrganizationRepo() ? `/organizations/${owner}/settings/deleted_repositories` : '/settings/deleted_repositories'; const otherForksURL = `/${owner}?tab=repositories&type=fork`; await addNotice( <> Repository {nameWithOwner} deleted. Restore it, visit the source repo, or see your other forks., {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}); } } async function init(signal: AbortSignal): Promise { if ( // Only if the user can delete the repository // TODO: Replace with https://github.com/refined-github/github-url-detection/issues/85 !await elementReady('nav [data-content="Settings"]') // Only if the repository hasn't been starred || looseParseInt(select('.starring-container .Counter')) > 0 ) { return false; } await api.expectToken(); // (Ab)use the details element as state and an accessible "click-anywhere-to-cancel" utility attachElement('.pagehead-actions', { prepend: () => (
  • {/* This extra element is needed to keep the button above the ’s lightbox */} Delete fork
  • ), }); delegate('.rgh-quick-repo-deletion[open]', 'toggle', handleToggle, {capture: true, signal}); } void features.add(import.meta.url, { include: [ pageDetect.isForkedRepo, ], init, });