import React, { ChangeEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { Button, ButtonVariation, Color, Container, FlexExpander, Icon, Layout, Text, TextInput } from '@harness/uicore' import { Link, useHistory } from 'react-router-dom' import ReactJoin from 'react-join' import cx from 'classnames' import { SourceCodeEditor } from 'components/SourceCodeEditor/SourceCodeEditor' import type { RepoFileContent } from 'services/code' import { useAppContext } from 'AppContext' import { GitCommitAction, GitContentType, GitInfoProps, isDir, makeDiffRefs } from 'utils/GitUtils' import { useStrings } from 'framework/strings' import { filenameToLanguage, FILE_SEPERATOR } from 'utils/Utils' import { useGetResourceContent } from 'hooks/useGetResourceContent' import { CommitModalButton } from 'components/CommitModalButton/CommitModalButton' import css from './FileEditor.module.scss' interface EditorProps extends Pick { resourceContent: GitInfoProps['resourceContent'] | null isRepositoryEmpty: boolean } function Editor({ resourceContent, repoMetadata, gitRef, resourcePath, isRepositoryEmpty }: EditorProps) { const history = useHistory() const inputRef = useRef() const isNew = useMemo(() => !resourceContent || isDir(resourceContent), [resourceContent]) const [fileName, setFileName] = useState(isNew ? '' : resourceContent?.name || '') const [parentPath, setParentPath] = useState( isNew ? resourcePath : resourcePath.split(FILE_SEPERATOR).slice(0, -1).join(FILE_SEPERATOR) ) const { getString } = useStrings() const { routes } = useAppContext() const [language, setLanguage] = useState(() => filenameToLanguage(fileName)) const [originalContent, setOriginalContent] = useState( window.atob((resourceContent?.content as RepoFileContent)?.data || '') ) const [content, setContent] = useState(originalContent) const fileResourcePath = useMemo( () => [(parentPath || '').trim(), (fileName || '').trim()].filter(p => !!p.trim()).join(FILE_SEPERATOR), [parentPath, fileName] ) const { data: folderContent, refetch: verifyFolder } = useGetResourceContent({ repoMetadata, gitRef, resourcePath: fileResourcePath, includeCommit: false, lazy: true }) const isUpdate = useMemo(() => resourcePath === fileResourcePath, [resourcePath, fileResourcePath]) const commitAction = useMemo( () => (isNew ? GitCommitAction.CREATE : isUpdate ? GitCommitAction.UPDATE : GitCommitAction.MOVE), [isNew, isUpdate] ) const [startVerifyFolder, setStartVerifyFolder] = useState(false) const rebuildPaths = useCallback(() => { const _tokens = fileName.split(FILE_SEPERATOR).filter(part => !!part.trim()) const _fileName = ((_tokens.pop() as string) || '').trim() const _parentPath = parentPath .split(FILE_SEPERATOR) .concat(_tokens) .map(p => p.trim()) .filter(part => !!part.trim()) .join(FILE_SEPERATOR) if (_fileName) { const normalizedFilename = _fileName.trim() const newLanguage = filenameToLanguage(normalizedFilename) if (normalizedFilename !== fileName) { setFileName(normalizedFilename) } // A workaround to force Monaco update content // with new language. Monaco still throws an error // textModel.js:178 Uncaught Error: Model is disposed! if (language !== newLanguage) { setLanguage(newLanguage) setOriginalContent(content) } } setParentPath(_parentPath) // Make API call to verify if fileResourcePath is an existing folder verifyFolder().then(() => setStartVerifyFolder(true)) }, [fileName, parentPath, language, content, verifyFolder]) // Calculate file name input field width based on number of characters inside useEffect(() => { if (inputRef.current) { inputRef.current.size = Math.min(Math.max(fileName.length - 2, 20), 50) } }, [fileName, inputRef]) // When file name is modified, verify if fileResourcePath is a folder. If it is // then rebuild parentPath and fileName (becomes empty) useEffect(() => { if (startVerifyFolder && folderContent?.type === GitContentType.DIR) { setStartVerifyFolder(false) setParentPath(fileResourcePath) setFileName('') } }, [startVerifyFolder, folderContent, fileResourcePath]) useEffect(() => { if (isNew) { // setName from click on empty repo page so either readme, license or gitignore const nameExists = window.location.href.includes('?name=') if (nameExists) { const fileName = window.location.href.split('?name=')[1] setFileName(fileName) } } }, [isNew]) return ( {/* {repoMetadata.uid} */} {parentPath && ( <> }> {parentPath.split(FILE_SEPERATOR).map((_path, index, paths) => { const pathAtIndex = paths.slice(0, index + 1).join('/') return ( {_path} ) })} )} (inputRef.current = ref)} wrapperClassName={css.inputContainer} placeholder={getString('nameYourFile')} onInput={(event: ChangeEvent) => { setFileName(event.currentTarget.value) }} onBlur={rebuildPaths} onFocus={({ target }) => { const value = (parentPath ? parentPath + FILE_SEPERATOR : '') + fileName setFileName(value) setParentPath('') setTimeout(() => { target.setSelectionRange(value.length, value.length) target.scrollLeft = Number.MAX_SAFE_INTEGER }, 0) }} /> {getString('in')} {gitRef} { if (newBranch) { history.replace( routes.toCODECompare({ repoPath: repoMetadata.path as string, diffRefs: makeDiffRefs(repoMetadata?.default_branch as string, newBranch) }) ) } else { history.push( routes.toCODERepository({ repoPath: repoMetadata.path as string, resourcePath: fileResourcePath, gitRef }) ) } setOriginalContent(content) }} disableBranchCreation={isRepositoryEmpty} />