6.7 KB179 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 {ErrorNotice} from 'isl-components/ErrorNotice';
10import {Icon} from 'isl-components/Icon';
11import {Kbd} from 'isl-components/Kbd';
12import {KeyCode, Modifier} from 'isl-components/KeyboardShortcuts';
13import {Subtle} from 'isl-components/Subtle';
14import {Tooltip} from 'isl-components/Tooltip';
15import {useAtom} from 'jotai';
16import {useEffect} from 'react';
17import {ComparisonType} from 'shared/Comparison';
18import serverAPI from './ClientToServerAPI';
19import {OpenComparisonViewButton} from './ComparisonView/OpenComparisonViewButton';
20import {FlexSpacer, Row} from './ComponentUtils';
21import {DropdownFields} from './DropdownFields';
22import {EmptyState} from './EmptyState';
23import {useCommandEvent} from './ISLShortcuts';
24import {OperationDisabledButton} from './OperationDisabledButton';
25import {ChangedFiles} from './UncommittedChanges';
26import {T, t} from './i18n';
27import {atomLoadableWithRefresh} from './jotaiUtils';
28import {DeleteShelveOperation} from './operations/DeleteShelveOperation';
29import {UnshelveOperation} from './operations/UnshelveOperation';
30import {RelativeDate} from './relativeDate';
31
32import './ShelvedChanges.css';
33
34const shelvedChangesState = atomLoadableWithRefresh(async _get => {
35 serverAPI.postMessage({
36 type: 'fetchShelvedChanges',
37 });
38
39 const result = await serverAPI.nextMessageMatching('fetchedShelvedChanges', () => true);
40 if (result.shelvedChanges.error != null) {
41 throw new Error(result.shelvedChanges.error.toString());
42 }
43 return result.shelvedChanges.value;
44});
45
46export function ShelvedChangesMenu() {
47 const additionalToggles = useCommandEvent('ToggleShelvedChangesDropdown');
48 return (
49 <Tooltip
50 component={dismiss => <ShelvedChangesList dismiss={dismiss} />}
51 trigger="click"
52 placement="bottom"
53 additionalToggles={additionalToggles.asEventTarget()}
54 group="topbar"
55 title={
56 <T replace={{$shortcut: <Kbd keycode={KeyCode.S} modifiers={[Modifier.ALT]} />}}>
57 Shelved Changes ($shortcut)
58 </T>
59 }>
60 <Button icon data-testid="shelved-changes-button">
61 <Icon icon="archive" />
62 </Button>
63 </Tooltip>
64 );
65}
66
67function ShelvedChangesList({dismiss}: {dismiss: () => void}) {
68 const [shelvedChanges, refresh] = useAtom(shelvedChangesState);
69 useEffect(() => {
70 // make sure we fetch whenever loading the shelved changes list
71 refresh();
72 }, [refresh]);
73 return (
74 <DropdownFields
75 title={
76 <Row>
77 <T>Shelved Changes</T>{' '}
78 <Tooltip
79 title={t(
80 'You can Shelve a set of uncommitted changes to save them for later, via the Shelve button in the list of uncommitted changes.\n\nHere you can view and Unshelve previously shelved changes.',
81 )}>
82 <Icon icon="info" />
83 </Tooltip>
84 </Row>
85 }
86 icon="archive"
87 className="shelved-changes-dropdown"
88 data-testid="shelved-changes-dropdown">
89 {shelvedChanges.state === 'loading' ? (
90 <Icon icon="loading" />
91 ) : shelvedChanges.state === 'hasError' ? (
92 <ErrorNotice
93 title="Could not fetch shelved changes"
94 error={shelvedChanges.error as Error}
95 />
96 ) : shelvedChanges.data.length === 0 ? (
97 <EmptyState small>
98 <T>No shelved changes</T>
99 </EmptyState>
100 ) : (
101 <div className="shelved-changes-list">
102 {shelvedChanges.data.map(change => {
103 const comparison = {
104 type: ComparisonType.Committed,
105 hash: change.hash,
106 };
107 return (
108 <div key={change.hash} className="shelved-changes-item">
109 <div className="shelved-changes-item-row">
110 <span className="shelve-name">{change.name}</span>
111 <Subtle>
112 <RelativeDate date={change.date} useShortVariant />
113 </Subtle>
114 <FlexSpacer />
115 <Tooltip title={t('Remove from the list of shelved changes')}>
116 <OperationDisabledButton
117 kind="icon"
118 contextKey={`delete-shelve-${change.hash}`}
119 data-testid={`delete-shelve-${change.hash}`}
120 className="unshelve-button"
121 runOperation={() => {
122 dismiss();
123 return new DeleteShelveOperation(change);
124 }}
125 icon={<Icon icon="trash" />}></OperationDisabledButton>
126 </Tooltip>
127 <Tooltip
128 title={t(
129 'Apply these changes without removing this from your list of shelved changes',
130 )}>
131 <OperationDisabledButton
132 kind="icon"
133 contextKey={`unshelve-keep-${change.hash}`}
134 className="unshelve-button"
135 runOperation={() => {
136 dismiss();
137 return new UnshelveOperation(change, true);
138 }}
139 icon={<Icon icon="layers-active" slot="start" />}>
140 <T>Apply</T>
141 </OperationDisabledButton>
142 </Tooltip>
143 <Tooltip
144 title={t(
145 'Apply these changes and remove this from your list of shelved changes',
146 )}>
147 <OperationDisabledButton
148 contextKey={`unshelve-${change.hash}`}
149 className="unshelve-button"
150 runOperation={() => {
151 dismiss();
152 return new UnshelveOperation(change, false);
153 }}
154 icon={<Icon icon="layers-active" slot="start" />}>
155 <T>Unshelve</T>
156 </OperationDisabledButton>
157 </Tooltip>
158 </div>
159 <OpenComparisonViewButton
160 comparison={comparison}
161 buttonText={<T>View Changes</T>}
162 onClick={dismiss}
163 />
164 <div className="shelved-changes-item-row">
165 <ChangedFiles
166 filesSubset={change.filesSample}
167 totalFiles={change.totalFileCount}
168 comparison={comparison}
169 />
170 </div>
171 </div>
172 );
173 })}
174 </div>
175 )}
176 </DropdownFields>
177 );
178}
179