8.3 KB272 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 {ReactNode} from 'react';
9import type {FieldConfig} from './types';
10
11import {Icon} from 'isl-components/Icon';
12import {extractTokens, TokensList} from 'isl-components/Tokens';
13import {DOCUMENTATION_DELAY, Tooltip} from 'isl-components/Tooltip';
14import {Fragment} from 'react';
15import {tracker} from '../analytics';
16import {Copyable} from '../Copyable';
17import {T} from '../i18n';
18import {RenderMarkup} from './RenderMarkup';
19import {SeeMoreContainer} from './SeeMoreContainer';
20import {CommitInfoTextArea} from './TextArea';
21import {CommitInfoTextField} from './TextField';
22import {convertFieldNameToKey, getOnClickToken, Section, SmallCapsTitle} from './utils';
23
24export function CommitInfoField({
25 field,
26 isBeingEdited,
27 readonly,
28 content,
29 editedField,
30 startEditingField,
31 setEditedField,
32 copyFromParent,
33 extra,
34 autofocus,
35}: {
36 field: FieldConfig;
37 isBeingEdited: boolean;
38 readonly: boolean;
39 startEditingField: () => void;
40 content?: string | Array<string>;
41 editedField: string | Array<string> | undefined;
42 setEditedField: (value: string) => unknown;
43 copyFromParent?: () => void;
44 extra?: JSX.Element;
45 autofocus?: boolean;
46}): JSX.Element | null {
47 const editedFieldContent =
48 editedField == null ? '' : Array.isArray(editedField) ? editedField.join(', ') : editedField;
49 if (field.type === 'title') {
50 return (
51 <>
52 {isBeingEdited ? (
53 <Section className="commit-info-title-field-section">
54 <SmallCapsTitle>
55 <Icon icon="milestone" />
56 <T>{field.key}</T>
57 </SmallCapsTitle>
58 <CommitInfoTextArea
59 kind={field.type}
60 name={field.key}
61 autoFocus={autofocus ?? false}
62 editedMessage={editedFieldContent}
63 setEditedField={setEditedField}
64 />
65 </Section>
66 ) : (
67 <div className="commit-info-title-wrapper">
68 <ClickToEditField
69 startEditingField={readonly ? undefined : startEditingField}
70 kind={field.type}
71 fieldKey={field.key}>
72 <span>{content}</span>
73 </ClickToEditField>
74 <div className="commit-info-field-buttons">
75 {readonly ? null : <EditFieldButton onClick={startEditingField} />}
76 {readonly || copyFromParent == null ? null : (
77 <CopyFromParentButton onClick={copyFromParent} />
78 )}
79 </div>
80 </div>
81 )}
82 {extra}
83 </>
84 );
85 } else {
86 const Wrapper =
87 field.type === 'textarea' || field.type === 'custom' ? SeeMoreContainer : Fragment;
88 if (field.type === 'read-only' && !content) {
89 // don't render empty read-only fields, since you can't "click to edit"
90 return null;
91 }
92
93 if (isBeingEdited) {
94 if (field.type === 'custom') {
95 const CustomEditorComponent = field.renderEditor;
96 return (
97 <Section className="commit-info-field-section">
98 <SmallCapsTitle>
99 <Icon icon={field.icon} />
100 {field.key}
101 </SmallCapsTitle>
102 <CustomEditorComponent
103 field={field}
104 content={editedFieldContent}
105 setEditedField={setEditedField}
106 autoFocus={autofocus ?? false}
107 />
108 {extra}
109 </Section>
110 );
111 } else if (field.type !== 'read-only') {
112 return (
113 <Section className="commit-info-field-section">
114 <SmallCapsTitle>
115 <Icon icon={field.icon} />
116 {field.key}
117 </SmallCapsTitle>
118 {field.type === 'field' ? (
119 <CommitInfoTextField
120 field={field}
121 autoFocus={autofocus ?? false}
122 editedMessage={editedFieldContent}
123 setEditedCommitMessage={setEditedField}
124 />
125 ) : (
126 <CommitInfoTextArea
127 kind={field.type}
128 name={field.key}
129 autoFocus={autofocus ?? false}
130 editedMessage={editedFieldContent}
131 setEditedField={setEditedField}
132 />
133 )}
134 {extra}
135 </Section>
136 );
137 }
138 }
139
140 let renderedContent;
141 if (content) {
142 if (field.type === 'custom') {
143 const CustomDisplayComponent = field.renderDisplay;
144 const fieldContent =
145 content == null ? '' : Array.isArray(content) ? content.join(', ') : content;
146 renderedContent = <CustomDisplayComponent content={fieldContent} />;
147 } else if (field.type === 'field') {
148 const tokens = Array.isArray(content) ? content : extractTokens(content)[0];
149 renderedContent = (
150 <div className="commit-info-tokenized-field">
151 <TokensList tokens={tokens} onClickToken={getOnClickToken(field)} />
152 {field.maxTokens === 1 && tokens.length > 0 && (
153 <Copyable iconOnly>{tokens[0]}</Copyable>
154 )}
155 </div>
156 );
157 } else {
158 if (Array.isArray(content) || !field.isRenderableMarkup) {
159 renderedContent = content;
160 } else {
161 renderedContent = <RenderMarkup>{content}</RenderMarkup>;
162 }
163 }
164 } else {
165 renderedContent = (
166 <span className="empty-description subtle">
167 {readonly ? (
168 <>
169 <T replace={{$name: field.key}}> No $name</T>
170 </>
171 ) : (
172 <>
173 <Icon icon="add" />
174 <T replace={{$name: field.key}}> Click to add $name</T>
175 </>
176 )}
177 </span>
178 );
179 }
180
181 return (
182 <Section>
183 <Wrapper>
184 <SmallCapsTitle>
185 <Icon icon={field.icon} />
186 <T>{field.key}</T>
187 <div className="commit-info-field-buttons">
188 {readonly ? null : <EditFieldButton onClick={startEditingField} />}
189 {readonly || copyFromParent == null ? null : (
190 <CopyFromParentButton onClick={copyFromParent} />
191 )}
192 </div>
193 </SmallCapsTitle>
194 <ClickToEditField
195 startEditingField={readonly ? undefined : startEditingField}
196 kind={field.type}
197 fieldKey={field.key}>
198 {renderedContent}
199 </ClickToEditField>
200 {extra}
201 </Wrapper>
202 </Section>
203 );
204 }
205}
206
207function ClickToEditField({
208 children,
209 startEditingField,
210 fieldKey,
211 kind,
212}: {
213 children: ReactNode;
214 /** function to run when you click to edit. If null, the entire field will be non-editable. */
215 startEditingField?: () => void;
216 fieldKey: string;
217 kind: 'title' | 'field' | 'textarea' | 'custom' | 'read-only';
218}) {
219 const editable = startEditingField != null && kind !== 'read-only';
220 const renderKey = convertFieldNameToKey(fieldKey);
221 return (
222 <div
223 className={`commit-info-rendered-${kind}${editable ? '' : ' non-editable'}`}
224 data-testid={`commit-info-rendered-${renderKey}`}
225 onClick={() => {
226 if (startEditingField != null && kind !== 'read-only') {
227 startEditingField();
228
229 tracker.track('CommitInfoFieldEditFieldClick', {
230 extras: {
231 fieldKey,
232 kind,
233 },
234 });
235 }
236 }}
237 onKeyPress={
238 startEditingField != null && kind !== 'read-only'
239 ? e => {
240 if (e.key === 'Enter' || e.key === ' ') {
241 startEditingField();
242 e.preventDefault();
243 }
244 }
245 : undefined
246 }
247 tabIndex={0}>
248 {children}
249 </div>
250 );
251}
252
253function EditFieldButton({onClick}: {onClick: () => void}) {
254 return (
255 <Tooltip title="Edit field" delayMs={DOCUMENTATION_DELAY}>
256 <button className="hover-edit-button" onClick={onClick}>
257 <Icon icon="edit" />
258 </button>
259 </Tooltip>
260 );
261}
262
263function CopyFromParentButton({onClick}: {onClick: () => void}) {
264 return (
265 <Tooltip title="Copy from previous commit" delayMs={DOCUMENTATION_DELAY}>
266 <button className="hover-edit-button" onClick={onClick}>
267 <Icon icon="clippy" />
268 </button>
269 </Tooltip>
270 );
271}
272