summaryrefslogtreecommitdiff
path: root/source/features/sync-pr-commit-title.tsx
blob: 48f6d41a4ab1fb3cfd529892d6a96db8a287f958 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
import React from 'dom-chef';
import select from 'select-dom';
import onetime from 'onetime';
import debounce from 'debounce-fn';
import delegate, {DelegateSubscription} from 'delegate-it';
import insertTextTextarea from 'insert-text-textarea';
import features from '../libs/features';
import onPrMergePanelOpen from '../libs/on-pr-merge-panel-open';

const commitTitleLimit = 72;
const prTitleFieldSelector = '[name="issue[title]"]';
const prTitleSubmitSelector = '.js-issue-update [type="submit"]';

const createCommitTitle = debounce<[], string>((): string => {
	const issueTitle = select('.js-issue-title')!.textContent!.trim();
	const issueInfo = ` (${getPRNumber()})`;
	const targetTitleLength = commitTitleLimit - issueInfo.length;

	if (issueTitle.length > targetTitleLength) {
		return issueTitle.substring(0, targetTitleLength - 1).trim() + '…' + issueInfo;
	}

	return issueTitle + issueInfo;
}, {
	wait: 1000,
	immediate: true
});

const getNote = onetime<[], HTMLElement>((): HTMLElement =>
	<p className="note">
		The title of this PR will be updated to match this title. <button type="button" className="btn-link muted-link text-underline" onClick={event => {
			deinit();
			event.currentTarget.parentElement!.remove(); // Hide note
		}}>Cancel</button>
	</p>
);

function getPRNumber(): string {
	return select('.gh-header-number')!.textContent!;
}

function maybeShowNote(): void {
	const inputField = select<HTMLInputElement>('#merge_title_field')!;
	const needsSubmission = createCommitTitle() !== inputField.value;

	if (needsSubmission) {
		if (select.all([prTitleFieldSelector, prTitleSubmitSelector].join()).length !== 2) {
			// Ensure that the required fields are there before adding the note
			throw new Error('Refined GitHub: `sync-pr-commit-title` can’t update the PR title');
		}

		inputField.after(getNote());
		return;
	}

	getNote().remove();
}

function submitPRTitleUpdate(): void {
	const inputField = select<HTMLInputElement>('#merge_title_field')!;

	// If the note isn't shown, the PR title doesn't need to be updated
	if (!getNote().isConnected) {
		return;
	}

	const prTitle = inputField.value.replace(new RegExp(`\\s*\\(${getPRNumber()}\\)$`), '');

	// Fill and submit title-change form
	select<HTMLInputElement>(prTitleFieldSelector)!.value = prTitle;
	select(prTitleSubmitSelector)!.click(); // `form.submit()` isn't sent via ajax
}

function onMergePanelOpen(event: Event): void {
	maybeShowNote();

	const field = select<HTMLTextAreaElement>('#merge_title_field')!;

	// Only if the user hasn't already interacted with it in this session
	if (field.closest('.is-dirty') || event.type === 'session:resume') {
		return;
	}

	// Replace default title and fire the correct events
	field.value = '';
	insertTextTextarea(field, createCommitTitle());
}

let listeners: DelegateSubscription[];
function init(): void {
	listeners = [
		...delegate('#discussion_bucket', '#merge_title_field', 'input', maybeShowNote),
		...delegate('#discussion_bucket', 'form.js-merge-pull-request', 'submit', submitPRTitleUpdate),
		...onPrMergePanelOpen(onMergePanelOpen)
	];
}

function deinit(): void {
	for (const delegation of listeners) {
		delegation.destroy();
	}

	listeners.length = 0;
}

features.add({
	id: __featureName__,
	description: 'Uses the PR’s title and description when merging and updates the PR’s title to the match the commit title, if changed.',
	screenshot: 'https://user-images.githubusercontent.com/1402241/51669708-9a712400-1ff7-11e9-913a-ac1ea1050975.png',
	include: [
		features.isPRConversation
	],
	load: features.onAjaxedPages,
	init
});