6.4 KB197 lines
Blame
1/**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8import {Button} from 'isl-components/Button';
9import {Checkbox} from 'isl-components/Checkbox';
10import {DatetimePicker} from 'isl-components/DatetimePicker';
11import {TextField} from 'isl-components/TextField';
12import {useEffect, useRef, useState} from 'react';
13import {tracker} from './analytics';
14import {t, T} from './i18n';
15import {GotoOperation} from './operations/GotoOperation';
16import {RebaseOperation} from './operations/RebaseOperation';
17import {useRunOperation} from './operationsState';
18import {type ExactRevset, exactRevset} from './types';
19
20import './GotoTimeMenu.css';
21
22/**
23 * Generates a succeedable revset for a specific number of hours ago
24 * @param hours Number of hours ago
25 * @returns A ExactRevset object
26 */
27function getRevsetForHoursAgo(hours: number): ExactRevset {
28 const date = new Date();
29 // setHours() correctly handles going back a day/month/year as needed, if it would take us from, eg, Jan 1st to Dec 31st
30 date.setHours(date.getHours() - hours);
31 const datetimeStr = formatDateTimeHelper(date);
32 return getRevsetForDate(datetimeStr);
33}
34
35/**
36 * Generates a succeedable revset for a specific date string
37 * @param dateString The date string to use in the revset
38 * @returns A ExactRevset object
39 */
40function getRevsetForDate(dateString: string): ExactRevset {
41 return exactRevset(`bsearch(date(">${dateString}"),max(public()))`);
42}
43
44/**
45 * Formats a date into a string compatible with the datetime-local input and SL revset.date()
46 * @param date The date to format
47 * @returns A string in the format YYYY-MM-DDTHH:MM
48 */
49function formatDateTimeHelper(date: Date): string {
50 // Date.toISOString() is close to what we want, but it has fractional seconds and isn't in local time
51 // Date.toLocaleString() is in local time, but uses slashes rather than dashes
52 // Format date as YYYY-MM-DD in local timezone
53 const year = date.getFullYear();
54 const month = String(date.getMonth() + 1).padStart(2, '0'); // getMonth() is 0-indexed
55 const day = String(date.getDate()).padStart(2, '0');
56
57 // Format time as HH:MM in local timezone
58 const hours = String(date.getHours()).padStart(2, '0');
59 const minutes = String(date.getMinutes()).padStart(2, '0');
60
61 // Return in format YYYY-MM-DDTHH:MM (required by datetime-local input and compatible with revset.date())
62 return `${year}-${month}-${day}T${hours}:${minutes}`;
63}
64
65/**
66 * GotoTimeContent component that can be used directly or wrapped in an expander
67 */
68export function GotoTimeContent({dismiss}: {dismiss?: () => unknown}) {
69 const runOperation = useRunOperation();
70 const [shouldRebase, setShouldRebase] = useState(false);
71 const [hours, setHours] = useState('');
72 const [datetime, setDatetime] = useState('');
73 const maxDatetime = useRef('');
74 const hoursInputRef = useRef(null);
75
76 useEffect(() => {
77 if (hoursInputRef.current) {
78 (hoursInputRef.current as HTMLInputElement).focus();
79 }
80
81 // Initialize datetime and maxDatetime with current time.
82 const now = new Date();
83 const nowFormatted = formatDateTimeHelper(now);
84 setDatetime(nowFormatted);
85 maxDatetime.current = nowFormatted;
86 }, [hoursInputRef]);
87
88 // When hours is edited, clear the datetime picker. Must be one or the other
89 const handleHoursChange = (value: string) => {
90 setHours(value);
91 if (value.trim().length > 0) {
92 setDatetime('');
93 }
94 };
95
96 // When datetime is edited, clear the hours input. Must be one or the other
97 const handleDatetimeChange = (value: string) => {
98 setDatetime(value);
99 if (value.trim().length > 0) {
100 setHours('');
101 }
102 };
103
104 const doGoToCommit = () => {
105 tracker.track('ClickGotoTimeButton', {
106 extras: {
107 isHours: hours.trim().length > 0,
108 shouldRebase,
109 },
110 });
111
112 // Get the destination revset based on "hours ago" or datetime, whichever has a value
113 let destinationRevset: ExactRevset;
114
115 if (hours.trim().length > 0) {
116 const hoursValue = parseFloat(hours);
117 if (isNaN(hoursValue)) {
118 return;
119 }
120 destinationRevset = getRevsetForHoursAgo(hoursValue);
121 } else if (datetime.trim().length > 0) {
122 destinationRevset = getRevsetForDate(datetime);
123 } else {
124 // No valid input
125 return;
126 }
127
128 if (shouldRebase) {
129 // Rebase current work onto the commit at the specified time
130 runOperation(new RebaseOperation(exactRevset('.'), destinationRevset));
131 } else {
132 // Go to the commit at the specified time
133 runOperation(new GotoOperation(destinationRevset));
134 }
135
136 // Dismiss the tooltip/dialog if it exists
137 if (dismiss) {
138 dismiss();
139 }
140 };
141
142 return (
143 <div className="goto-time-content">
144 <div className="goto-time-input-row">
145 <TextField
146 width="100%"
147 placeholder={t('Hours ago')}
148 value={hours}
149 data-testid="goto-time-input"
150 onInput={e => handleHoursChange((e.target as unknown as {value: string})?.value ?? '')}
151 onKeyDown={e => {
152 if (e.key === 'Enter') {
153 if (hours.trim().length > 0) {
154 doGoToCommit();
155 }
156 }
157 }}
158 ref={hoursInputRef}
159 />
160 </div>
161
162 <div className="goto-time-or-divider">
163 <T>or</T>
164 </div>
165
166 <div className="goto-time-datetime-inputs">
167 <DatetimePicker
168 width="100%"
169 value={datetime}
170 max={maxDatetime.current}
171 onInput={e => handleDatetimeChange((e.target as unknown as {value: string})?.value ?? '')}
172 onKeyDown={e => {
173 if (e.key === 'Enter' && datetime.trim().length > 0) {
174 doGoToCommit();
175 } else if (datetime.trim().length === 0) {
176 setHours(''); // Clear hours on keyDown, not just onInput, which is only fired for a complete/valid date
177 }
178 }}
179 />
180 </div>
181
182 <div className="goto-time-actions">
183 <Checkbox checked={shouldRebase} onChange={setShouldRebase}>
184 <T>Rebase current stack here</T>
185 </Checkbox>
186 <Button
187 data-testid="goto-time-button"
188 primary
189 disabled={hours.trim().length === 0 && datetime.trim().length === 0}
190 onClick={doGoToCommit}>
191 <T>Goto</T>
192 </Button>
193 </div>
194 </div>
195 );
196}
197