addons/isl/src/CommitInfoView/CommitInfoField.tsxblame
View source
b69ab311/**
b69ab312 * Copyright (c) Meta Platforms, Inc. and affiliates.
b69ab313 *
b69ab314 * This source code is licensed under the MIT license found in the
b69ab315 * LICENSE file in the root directory of this source tree.
b69ab316 */
b69ab317
b69ab318import type {ReactNode} from 'react';
b69ab319import type {FieldConfig} from './types';
b69ab3110
b69ab3111import {Icon} from 'isl-components/Icon';
b69ab3112import {extractTokens, TokensList} from 'isl-components/Tokens';
b69ab3113import {DOCUMENTATION_DELAY, Tooltip} from 'isl-components/Tooltip';
b69ab3114import {Fragment} from 'react';
b69ab3115import {tracker} from '../analytics';
b69ab3116import {Copyable} from '../Copyable';
b69ab3117import {T} from '../i18n';
b69ab3118import {RenderMarkup} from './RenderMarkup';
b69ab3119import {SeeMoreContainer} from './SeeMoreContainer';
b69ab3120import {CommitInfoTextArea} from './TextArea';
b69ab3121import {CommitInfoTextField} from './TextField';
b69ab3122import {convertFieldNameToKey, getOnClickToken, Section, SmallCapsTitle} from './utils';
b69ab3123
b69ab3124export function CommitInfoField({
b69ab3125 field,
b69ab3126 isBeingEdited,
b69ab3127 readonly,
b69ab3128 content,
b69ab3129 editedField,
b69ab3130 startEditingField,
b69ab3131 setEditedField,
b69ab3132 copyFromParent,
b69ab3133 extra,
b69ab3134 autofocus,
b69ab3135}: {
b69ab3136 field: FieldConfig;
b69ab3137 isBeingEdited: boolean;
b69ab3138 readonly: boolean;
b69ab3139 startEditingField: () => void;
b69ab3140 content?: string | Array<string>;
b69ab3141 editedField: string | Array<string> | undefined;
b69ab3142 setEditedField: (value: string) => unknown;
b69ab3143 copyFromParent?: () => void;
b69ab3144 extra?: JSX.Element;
b69ab3145 autofocus?: boolean;
b69ab3146}): JSX.Element | null {
b69ab3147 const editedFieldContent =
b69ab3148 editedField == null ? '' : Array.isArray(editedField) ? editedField.join(', ') : editedField;
b69ab3149 if (field.type === 'title') {
b69ab3150 return (
b69ab3151 <>
b69ab3152 {isBeingEdited ? (
b69ab3153 <Section className="commit-info-title-field-section">
b69ab3154 <SmallCapsTitle>
b69ab3155 <Icon icon="milestone" />
b69ab3156 <T>{field.key}</T>
b69ab3157 </SmallCapsTitle>
b69ab3158 <CommitInfoTextArea
b69ab3159 kind={field.type}
b69ab3160 name={field.key}
b69ab3161 autoFocus={autofocus ?? false}
b69ab3162 editedMessage={editedFieldContent}
b69ab3163 setEditedField={setEditedField}
b69ab3164 />
b69ab3165 </Section>
b69ab3166 ) : (
b69ab3167 <div className="commit-info-title-wrapper">
b69ab3168 <ClickToEditField
b69ab3169 startEditingField={readonly ? undefined : startEditingField}
b69ab3170 kind={field.type}
b69ab3171 fieldKey={field.key}>
b69ab3172 <span>{content}</span>
b69ab3173 </ClickToEditField>
b69ab3174 <div className="commit-info-field-buttons">
b69ab3175 {readonly ? null : <EditFieldButton onClick={startEditingField} />}
b69ab3176 {readonly || copyFromParent == null ? null : (
b69ab3177 <CopyFromParentButton onClick={copyFromParent} />
b69ab3178 )}
b69ab3179 </div>
b69ab3180 </div>
b69ab3181 )}
b69ab3182 {extra}
b69ab3183 </>
b69ab3184 );
b69ab3185 } else {
b69ab3186 const Wrapper =
b69ab3187 field.type === 'textarea' || field.type === 'custom' ? SeeMoreContainer : Fragment;
b69ab3188 if (field.type === 'read-only' && !content) {
b69ab3189 // don't render empty read-only fields, since you can't "click to edit"
b69ab3190 return null;
b69ab3191 }
b69ab3192
b69ab3193 if (isBeingEdited) {
b69ab3194 if (field.type === 'custom') {
b69ab3195 const CustomEditorComponent = field.renderEditor;
b69ab3196 return (
b69ab3197 <Section className="commit-info-field-section">
b69ab3198 <SmallCapsTitle>
b69ab3199 <Icon icon={field.icon} />
b69ab31100 {field.key}
b69ab31101 </SmallCapsTitle>
b69ab31102 <CustomEditorComponent
b69ab31103 field={field}
b69ab31104 content={editedFieldContent}
b69ab31105 setEditedField={setEditedField}
b69ab31106 autoFocus={autofocus ?? false}
b69ab31107 />
b69ab31108 {extra}
b69ab31109 </Section>
b69ab31110 );
b69ab31111 } else if (field.type !== 'read-only') {
b69ab31112 return (
b69ab31113 <Section className="commit-info-field-section">
b69ab31114 <SmallCapsTitle>
b69ab31115 <Icon icon={field.icon} />
b69ab31116 {field.key}
b69ab31117 </SmallCapsTitle>
b69ab31118 {field.type === 'field' ? (
b69ab31119 <CommitInfoTextField
b69ab31120 field={field}
b69ab31121 autoFocus={autofocus ?? false}
b69ab31122 editedMessage={editedFieldContent}
b69ab31123 setEditedCommitMessage={setEditedField}
b69ab31124 />
b69ab31125 ) : (
b69ab31126 <CommitInfoTextArea
b69ab31127 kind={field.type}
b69ab31128 name={field.key}
b69ab31129 autoFocus={autofocus ?? false}
b69ab31130 editedMessage={editedFieldContent}
b69ab31131 setEditedField={setEditedField}
b69ab31132 />
b69ab31133 )}
b69ab31134 {extra}
b69ab31135 </Section>
b69ab31136 );
b69ab31137 }
b69ab31138 }
b69ab31139
b69ab31140 let renderedContent;
b69ab31141 if (content) {
b69ab31142 if (field.type === 'custom') {
b69ab31143 const CustomDisplayComponent = field.renderDisplay;
b69ab31144 const fieldContent =
b69ab31145 content == null ? '' : Array.isArray(content) ? content.join(', ') : content;
b69ab31146 renderedContent = <CustomDisplayComponent content={fieldContent} />;
b69ab31147 } else if (field.type === 'field') {
b69ab31148 const tokens = Array.isArray(content) ? content : extractTokens(content)[0];
b69ab31149 renderedContent = (
b69ab31150 <div className="commit-info-tokenized-field">
b69ab31151 <TokensList tokens={tokens} onClickToken={getOnClickToken(field)} />
b69ab31152 {field.maxTokens === 1 && tokens.length > 0 && (
b69ab31153 <Copyable iconOnly>{tokens[0]}</Copyable>
b69ab31154 )}
b69ab31155 </div>
b69ab31156 );
b69ab31157 } else {
b69ab31158 if (Array.isArray(content) || !field.isRenderableMarkup) {
b69ab31159 renderedContent = content;
b69ab31160 } else {
b69ab31161 renderedContent = <RenderMarkup>{content}</RenderMarkup>;
b69ab31162 }
b69ab31163 }
b69ab31164 } else {
b69ab31165 renderedContent = (
b69ab31166 <span className="empty-description subtle">
b69ab31167 {readonly ? (
b69ab31168 <>
b69ab31169 <T replace={{$name: field.key}}> No $name</T>
b69ab31170 </>
b69ab31171 ) : (
b69ab31172 <>
b69ab31173 <Icon icon="add" />
b69ab31174 <T replace={{$name: field.key}}> Click to add $name</T>
b69ab31175 </>
b69ab31176 )}
b69ab31177 </span>
b69ab31178 );
b69ab31179 }
b69ab31180
b69ab31181 return (
b69ab31182 <Section>
b69ab31183 <Wrapper>
b69ab31184 <SmallCapsTitle>
b69ab31185 <Icon icon={field.icon} />
b69ab31186 <T>{field.key}</T>
b69ab31187 <div className="commit-info-field-buttons">
b69ab31188 {readonly ? null : <EditFieldButton onClick={startEditingField} />}
b69ab31189 {readonly || copyFromParent == null ? null : (
b69ab31190 <CopyFromParentButton onClick={copyFromParent} />
b69ab31191 )}
b69ab31192 </div>
b69ab31193 </SmallCapsTitle>
b69ab31194 <ClickToEditField
b69ab31195 startEditingField={readonly ? undefined : startEditingField}
b69ab31196 kind={field.type}
b69ab31197 fieldKey={field.key}>
b69ab31198 {renderedContent}
b69ab31199 </ClickToEditField>
b69ab31200 {extra}
b69ab31201 </Wrapper>
b69ab31202 </Section>
b69ab31203 );
b69ab31204 }
b69ab31205}
b69ab31206
b69ab31207function ClickToEditField({
b69ab31208 children,
b69ab31209 startEditingField,
b69ab31210 fieldKey,
b69ab31211 kind,
b69ab31212}: {
b69ab31213 children: ReactNode;
b69ab31214 /** function to run when you click to edit. If null, the entire field will be non-editable. */
b69ab31215 startEditingField?: () => void;
b69ab31216 fieldKey: string;
b69ab31217 kind: 'title' | 'field' | 'textarea' | 'custom' | 'read-only';
b69ab31218}) {
b69ab31219 const editable = startEditingField != null && kind !== 'read-only';
b69ab31220 const renderKey = convertFieldNameToKey(fieldKey);
b69ab31221 return (
b69ab31222 <div
b69ab31223 className={`commit-info-rendered-${kind}${editable ? '' : ' non-editable'}`}
b69ab31224 data-testid={`commit-info-rendered-${renderKey}`}
b69ab31225 onClick={() => {
b69ab31226 if (startEditingField != null && kind !== 'read-only') {
b69ab31227 startEditingField();
b69ab31228
b69ab31229 tracker.track('CommitInfoFieldEditFieldClick', {
b69ab31230 extras: {
b69ab31231 fieldKey,
b69ab31232 kind,
b69ab31233 },
b69ab31234 });
b69ab31235 }
b69ab31236 }}
b69ab31237 onKeyPress={
b69ab31238 startEditingField != null && kind !== 'read-only'
b69ab31239 ? e => {
b69ab31240 if (e.key === 'Enter' || e.key === ' ') {
b69ab31241 startEditingField();
b69ab31242 e.preventDefault();
b69ab31243 }
b69ab31244 }
b69ab31245 : undefined
b69ab31246 }
b69ab31247 tabIndex={0}>
b69ab31248 {children}
b69ab31249 </div>
b69ab31250 );
b69ab31251}
b69ab31252
b69ab31253function EditFieldButton({onClick}: {onClick: () => void}) {
b69ab31254 return (
b69ab31255 <Tooltip title="Edit field" delayMs={DOCUMENTATION_DELAY}>
b69ab31256 <button className="hover-edit-button" onClick={onClick}>
b69ab31257 <Icon icon="edit" />
b69ab31258 </button>
b69ab31259 </Tooltip>
b69ab31260 );
b69ab31261}
b69ab31262
b69ab31263function CopyFromParentButton({onClick}: {onClick: () => void}) {
b69ab31264 return (
b69ab31265 <Tooltip title="Copy from previous commit" delayMs={DOCUMENTATION_DELAY}>
b69ab31266 <button className="hover-edit-button" onClick={onClick}>
b69ab31267 <Icon icon="clippy" />
b69ab31268 </button>
b69ab31269 </Tooltip>
b69ab31270 );
b69ab31271}