3.9 KB138 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, RefObject} from 'react';
9
10import {TextArea} from 'isl-components/TextArea';
11import {useEffect, useRef} from 'react';
12import {InternalFieldName} from 'shared/constants';
13import {
14 FilePicker,
15 ImageDropZone,
16 PendingImageUploads,
17 useUploadFilesCallback,
18} from '../ImageUpload';
19import {Internal} from '../Internal';
20import {MinHeightTextField} from './MinHeightTextField';
21import {convertFieldNameToKey} from './utils';
22
23function moveCursorToEnd(element: HTMLTextAreaElement) {
24 element.setSelectionRange(element.value.length, element.value.length);
25}
26
27export function CommitInfoTextArea({
28 kind,
29 name,
30 autoFocus,
31 editedMessage,
32 setEditedField,
33}: {
34 kind: 'title' | 'textarea' | 'field';
35 name: string;
36 autoFocus: boolean;
37 editedMessage: string;
38 setEditedField: (fieldValue: string) => unknown;
39}) {
40 const ref = useRef<HTMLTextAreaElement>(null);
41 useEffect(() => {
42 if (ref.current && autoFocus) {
43 ref.current.focus();
44 moveCursorToEnd(ref.current);
45 }
46 }, [autoFocus, ref]);
47 const Component = kind === 'field' || kind === 'title' ? MinHeightTextField : TextArea;
48 const props =
49 kind === 'field' || kind === 'title'
50 ? {}
51 : ({
52 rows: 15,
53 resize: 'vertical',
54 } as const);
55
56 // The gh cli does not support uploading images for commit messages,
57 // see https://github.com/cli/cli/issues/1895#issuecomment-718899617
58 // for now, this is internal-only.
59 const supportsImageUpload =
60 kind === 'textarea' &&
61 (Internal.supportsImageUpload === true ||
62 // image upload is always enabled in tests
63 process.env.NODE_ENV === 'test');
64
65 const onInput = (event: {currentTarget: HTMLTextAreaElement}) => {
66 setEditedField(event.currentTarget?.value);
67 };
68
69 const uploadFiles = useUploadFilesCallback(name, ref, onInput);
70
71 const fieldKey = convertFieldNameToKey(name);
72
73 const rendered = (
74 <div className="commit-info-field">
75 <Component
76 ref={ref}
77 {...props}
78 onPaste={
79 !supportsImageUpload
80 ? undefined
81 : (event: React.ClipboardEvent<HTMLTextAreaElement>) => {
82 if (event.clipboardData != null && event.clipboardData.files.length > 0) {
83 uploadFiles([...event.clipboardData.files]);
84 event.preventDefault();
85 }
86 }
87 }
88 value={editedMessage}
89 data-testid={`commit-info-${fieldKey}-field`}
90 onInput={onInput}
91 />
92 <EditorToolbar
93 fieldName={name}
94 uploadFiles={supportsImageUpload ? uploadFiles : undefined}
95 textAreaRef={ref}
96 />
97 </div>
98 );
99 return !supportsImageUpload ? (
100 rendered
101 ) : (
102 <ImageDropZone onDrop={uploadFiles}>{rendered}</ImageDropZone>
103 );
104}
105
106/**
107 * Floating button list at the bottom corner of the text area
108 */
109export function EditorToolbar({
110 fieldName,
111 textAreaRef,
112 uploadFiles,
113}: {
114 fieldName: string;
115 uploadFiles?: (files: Array<File>) => unknown;
116 textAreaRef: RefObject<HTMLTextAreaElement>;
117}) {
118 const parts: Array<ReactNode> = [];
119 if (uploadFiles != null) {
120 parts.push(
121 <PendingImageUploads fieldName={fieldName} key="pending-uploads" textAreaRef={textAreaRef} />,
122 );
123 }
124 if (fieldName === InternalFieldName.TestPlan && Internal.RecommendTestPlanButton) {
125 parts.push(<Internal.RecommendTestPlanButton key="recommend-test-plan" />);
126 }
127 if (fieldName === InternalFieldName.Summary && Internal.GenerateSummaryButton) {
128 parts.push(<Internal.GenerateSummaryButton key="generate-summary" />);
129 }
130 if (uploadFiles != null) {
131 parts.push(<FilePicker key="picker" uploadFiles={uploadFiles} />);
132 }
133 if (parts.length === 0) {
134 return null;
135 }
136 return <div className="text-area-toolbar">{parts}</div>;
137}
138