/
githubmirror
/
gitness
Обзор
Документация
Войти
/
githubmirror
/
gitness
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
web/src/pages/RepositoryFileEdit/FileEditor/FileEditor.tsx
306 строк
11 KB
Ritik Kapoor
fix: [CODE-3280] update target ref in pr compare path (#3720)
22 апр 2025, 22:18
22 апр 2025, 22:18
0ba7be9
Код
Авторство
О чём код?
/* * 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<GitInfoProps, 'repoMetadata' | 'gitRef' | 'resourcePath'> { 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<HTMLInputElement | null>() 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 ( <Container className={css.container}> <Layout.Horizontal className={css.heading}> <Container style={{ maxWidth: 'calc(var(--page-container-width) - 300px)' }}> <Layout.Horizontal spacing="small" className={css.path}> <Link to={routes.toCODERepository({ repoPath: repoMetadata.path as string, gitRef })}> <Icon name="code-folder" padding={{ right: 'xsmall' }} /> </Link> <PathSeparator /> {parentPath && ( <> <Breadcrumbs items={breadcrumbs} breadcrumbRenderer={({ text, href }: IBreadcrumbProps) => { return ( <Link to={href as string}> <Text color={Color.GREY_900}>{text}</Text> </Link> ) }} /> <PathSeparator /> </> )} <TextInput autoFocus={isNew} value={fileName} inputRef={_ref => (inputRef.current = _ref)} wrapperClassName={css.inputContainer} placeholder={getString('nameYourFile')} onInput={(event: ChangeEvent<HTMLInputElement>) => { 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) }} /> <Text color={Color.GREY_900}>{getString('in')}</Text> <Link to={routes.toCODERepository({ repoPath: repoMetadata.path as string, gitRef })} className={css.refLink}> {gitRef} </Link> </Layout.Horizontal> </Container> <FlexExpander /> <Container> <Layout.Horizontal spacing="small"> <CommitModalButton text={getString('commitChanges')} variation={ButtonVariation.PRIMARY} disabled={!dirty} repoMetadata={repoMetadata} commitAction={commitAction} commitTitlePlaceHolder={getString(isNew ? 'createFile' : isUpdate ? 'updateFile' : 'renameFile') .replace('__path__', isUpdate || isNew ? fileResourcePath : resourcePath) .replace('__newPath__', fileResourcePath)} gitRef={gitRef} oldResourcePath={commitAction === GitCommitAction.MOVE ? resourcePath : undefined} resourcePath={fileResourcePath} payload={content} sha={resourceContent?.sha} onSuccess={(_data, newBranch) => { setDirty(false) if (newBranch) { history.replace( routes.toCODECompare({ repoPath: repoMetadata.path as string, diffRefs: makeDiffRefs((gitRef ?? repoMetadata?.default_branch) as string, newBranch) }) ) } else { history.push( routes.toCODERepository({ repoPath: repoMetadata.path as string, resourcePath: fileResourcePath, gitRef }) ) } setOriginalContent(content) }} disableBranchCreation={isRepositoryEmpty} /> <Button text={getString('cancel')} variation={ButtonVariation.TERTIARY} onClick={() => { history.replace( routes.toCODERepository({ repoPath: repoMetadata.path as string, gitRef, resourcePath }) ) }} /> </Layout.Horizontal> </Container> </Layout.Horizontal> <Container className={css.tabs}> <VisualYamlToggle onChange={setSelectedView} selectedView={selectedView} labels={{ visual: getString('contents'), yaml: getString('changes') }} className={css.selectedView} /> </Container> <Container className={cx(css.editorContainer, language)}> {selectedView === VisualYamlSelectedView.VISUAL ? ( <SourceCodeEditor language={language} source={content} onChange={setContent} /> ) : ( <DiffEditor language={language} original={originalContent} source={content} onChange={setContent} /> )} </Container> <NavigationCheck when={dirty} /> </Container> ) } const PathSeparator = () => <Text color={Color.GREY_900}>/</Text> export const FileEditor = React.memo(Editor)