5.6 KB166 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 type {MutableRefObject} from 'react';
9import type {FieldsBeingEdited} from '../../CommitInfoView/types';
10import type {CommitInfo} from '../../types';
11
12import {Button} from 'isl-components/Button';
13import {Divider} from 'isl-components/Divider';
14import {Icon} from 'isl-components/Icon';
15import {useAtomValue} from 'jotai';
16import {useCallback} from 'react';
17import {useAutofocusRef} from 'shared/hooks';
18import {Commit} from '../../Commit';
19import {
20 editedCommitMessages,
21 getDefaultEditedCommitMessage,
22 unsavedFieldsBeingEdited,
23} from '../../CommitInfoView/CommitInfoState';
24import {commitMessageFieldsSchema} from '../../CommitInfoView/CommitMessageFields';
25import {FlexSpacer} from '../../ComponentUtils';
26import {T, t} from '../../i18n';
27import {readAtom, writeAtom} from '../../jotaiUtils';
28import {CommitPreview} from '../../previews';
29import {useModal} from '../../useModal';
30
31import './ConfirmUnsavedEditsBeforeSplit.css';
32
33type UnsavedEditConfirmKind = 'split' | 'edit_stack';
34
35export function useConfirmUnsavedEditsBeforeSplit(): (
36 commits: Array<CommitInfo>,
37 kind: UnsavedEditConfirmKind,
38) => Promise<boolean> {
39 const showModal = useModal();
40 const showConfirmation = useCallback(
41 async (commits: Array<CommitInfo>, kind: UnsavedEditConfirmKind): Promise<boolean> => {
42 const editedCommits = commits
43 .map(commit => [commit, readAtom(unsavedFieldsBeingEdited(commit.hash))])
44 .filter(([_, f]) => f != null) as Array<[CommitInfo, FieldsBeingEdited]>;
45 if (editedCommits.some(([_, f]) => Object.values(f).some(Boolean))) {
46 const continueWithSplit = await showModal<boolean>({
47 type: 'custom',
48 component: ({returnResultAndDismiss}) => (
49 <PreSplitUnsavedEditsConfirmationModal
50 kind={kind}
51 editedCommits={editedCommits}
52 returnResultAndDismiss={returnResultAndDismiss}
53 />
54 ),
55 title:
56 kind === 'split'
57 ? t('Save edits before splitting?')
58 : t('Save edits before editing stack?'),
59 });
60 return continueWithSplit === true;
61 }
62 return true;
63 },
64 [showModal],
65 );
66
67 return (commits: Array<CommitInfo>, kind: UnsavedEditConfirmKind) => {
68 return showConfirmation(commits, kind);
69 };
70}
71
72function PreSplitUnsavedEditsConfirmationModal({
73 kind,
74 editedCommits,
75 returnResultAndDismiss,
76}: {
77 kind: UnsavedEditConfirmKind;
78 editedCommits: Array<[CommitInfo, FieldsBeingEdited]>;
79 returnResultAndDismiss: (continueWithSplit: boolean) => unknown;
80}) {
81 const schema = useAtomValue(commitMessageFieldsSchema);
82
83 const resetEditedCommitMessage = useCallback((commit: CommitInfo) => {
84 writeAtom(editedCommitMessages(commit.hash), getDefaultEditedCommitMessage());
85 }, []);
86
87 const commitsWithUnsavedEdits = editedCommits.filter(([_, fields]) =>
88 Object.values(fields).some(Boolean),
89 );
90
91 const saveButtonRef = useAutofocusRef();
92
93 return (
94 <div className="confirm-unsaved-edits-pre-split" data-testid="confirm-unsaved-edits-pre-split">
95 <>
96 <div>
97 <T count={commitsWithUnsavedEdits.length}>
98 {kind === 'split'
99 ? 'confirmUnsavedEditsBeforeSplit'
100 : 'confirmUnsavedEditsBeforeEditStack'}
101 </T>
102 </div>
103 <div className="commits-with-unsaved-changes">
104 {commitsWithUnsavedEdits.map(([commit, fields]) => (
105 <div className="commit-row" key={commit.hash}>
106 <Commit
107 commit={commit}
108 hasChildren={false}
109 previewType={CommitPreview.NON_ACTIONABLE_COMMIT}
110 />
111 <span key={`${commit.hash}-fields`} className="byline">
112 <T
113 replace={{
114 $commitTitle: commit.title,
115 $fields: (
116 <>
117 {Object.entries(fields)
118 .filter(([, value]) => value)
119 .map(([field]) => {
120 const icon = schema.find(f => f.key === field)?.icon;
121 return (
122 <span key={field} className="field-name">
123 {icon && <Icon icon={icon} />}
124 {field}
125 </span>
126 );
127 })}
128 </>
129 ),
130 }}>
131 unsaved changes to $fields
132 </T>
133 </span>
134 </div>
135 ))}
136 </div>
137 <Divider />
138 <div className="use-modal-buttons">
139 <FlexSpacer />
140 <Button onClick={() => returnResultAndDismiss(false)}>
141 <T>Cancel</T>
142 </Button>
143 <Button
144 onClick={() => {
145 for (const [commit] of editedCommits) {
146 resetEditedCommitMessage(commit);
147 }
148 returnResultAndDismiss(true); // continue with split
149 }}>
150 <T>Discard Edits</T>
151 </Button>
152 <Button
153 ref={saveButtonRef as MutableRefObject<null>}
154 primary
155 onClick={() => {
156 // Unsaved edits will be automatically loaded by the split as the commits' text
157 returnResultAndDismiss(true); // continue with split
158 }}>
159 <T>Save Edits</T>
160 </Button>
161 </div>
162 </>
163 </div>
164 );
165}
166