/* * Copyright 2023 Harness, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import React, { ChangeEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { Button, ButtonVariation, Container, FlexExpander, Layout, Text, TextInput, VisualYamlSelectedView, VisualYamlToggle } from '@harnessio/uicore' import { Icon } from '@harnessio/icons' import { Color } from '@harnessio/design-system' import { Link, useHistory } from 'react-router-dom' import { Breadcrumbs, IBreadcrumbProps } from '@blueprintjs/core' import cx from 'classnames' import { SourceCodeEditor } from 'components/SourceCodeEditor/SourceCodeEditor' import type { RepoFileContent } from 'services/code' import { useAppContext } from 'AppContext' import { normalizeGitRef, decodeGitContent, GitCommitAction, GitContentType, GitInfoProps, isDir, makeDiffRefs } from 'utils/GitUtils' import { useStrings } from 'framework/strings' import { filenameToLanguage, FILE_SEPARATOR } from 'utils/Utils' import { useGetResourceContent } from 'hooks/useGetResourceContent' import { CommitModalButton } from 'components/CommitModalButton/CommitModalButton' import { DiffEditor } from 'components/SourceCodeEditor/MonacoSourceCodeEditor' import { NavigationCheck } from 'components/NavigationCheck/NavigationCheck' 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 name = new URLSearchParams(window.location.href.split('?')?.[1]).get('name') 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_SEPARATOR).slice(0, -1).join(FILE_SEPARATOR) ) const { getString } = useStrings() const { routes } = useAppContext() const [language, setLanguage] = useState(() => filenameToLanguage(fileName)) const [originalContent, setOriginalContent] = useState( decodeGitContent((resourceContent?.content as RepoFileContent)?.data) ) const [content, setContent] = useState(originalContent) const fileResourcePath = useMemo( () => [(parentPath || '').trim(), (fileName || '').trim()].filter(p => !!p.trim()).join(FILE_SEPARATOR), [parentPath, fileName] ) const { data: folderContent, refetch: verifyFolder } = useGetResourceContent({ repoMetadata, gitRef: normalizeGitRef(gitRef) as string, 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_SEPARATOR).filter(part => !!part.trim()) const _fileName = ((_tokens.pop() as string) || '').trim() const _parentPath = parentPath .split(FILE_SEPARATOR) .concat(_tokens) .map(p => p.trim()) .filter(part => !!part.trim()) .join(FILE_SEPARATOR) 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]) const [selectedView, setSelectedView] = useState(VisualYamlSelectedView.VISUAL) const [dirty, setDirty] = useState(false) const breadcrumbs = useMemo(() => { return parentPath.split('/').map((_path, index, paths) => { const pathAtIndex = paths.slice(0, index + 1).join('/') const href = routes.toCODERepository({ repoPath: repoMetadata.path as string, gitRef, resourcePath: pathAtIndex }) return { href, text: _path } }) }, [parentPath, gitRef, repoMetadata.path, routes]) useEffect(() => { setDirty(!(!fileName || (isUpdate && content === originalContent))) }, [fileName, isUpdate, content, originalContent]) // 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 && !!name) { // setName from click on empty repo page so either readme, license or gitignore const nameExists = name if (nameExists !== '') { const newFilename = name setFileName(newFilename) } } }, [isNew, name]) return ( {parentPath && ( <> { return ( {text} ) }} /> )} (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_SEPARATOR : '') + fileName setFileName(value) setParentPath('') setTimeout(() => { target.setSelectionRange(value.length, value.length) target.scrollLeft = Number.MAX_SAFE_INTEGER }, 0) }} /> {getString('in')} {gitRef} { setDirty(false) 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} />