/* * Copyright (c) 2020 - present Cloudogu GmbH * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU Affero General Public License as published by the Free * Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more * details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see https://www.gnu.org/licenses/. */ import React, { FC, useCallback, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import classNames from "classnames"; import { ButtonGroup } from "../../buttons"; import { Icon } from "@scm-manager/ui-buttons"; import { Hunk as HunkType, Link } from "@scm-manager/ui-types"; import TokenizedDiffView from "../TokenizedDiffView"; import DiffButton from "../DiffButton"; import { MenuContext } from "../../navigation/MenuContext"; import OpenInFullscreenButton from "../../buttons/OpenInFullscreenButton"; import DiffExpander from "../DiffExpander"; import { Modal } from "../../modals"; import { ErrorNotification } from "@scm-manager/ui-core"; import FileTitle from "./FileTitle"; import { DiffFilePanel, FullWidthTitleHeader, MarginlessModalContent, PanelHeading } from "./styledElements"; import ChangeTag from "./ChangeTag"; import { getAnchorId, hasContent as determineHasContent, hoverFileTitle } from "./helpers"; import DiffFileHunk from "./DiffFileHunk"; import { DiffFileProps } from "./types"; import { useContentType } from "@scm-manager/ui-api"; import BinaryDiffFileContent, { canDisplayBinaryFile } from "./BinaryDiffFileContent"; import styled from "styled-components"; type Props = DiffFileProps; const Filepath = styled.h4` word-break: break-all; `; const DiffFile: FC = ({ file: fileProp, isCollapsed: isCollapsedProp, onCollapseStateChange, defaultCollapse: defaultCollapseProp, stickyHeader, sideBySide: sideBySideProp, whitespace: whitespaceProp, markConflicts = true, fileControlFactory, fileAnnotationFactory, onClick, annotationFactory, hunkGutterHoverIcon, hunkClass, highlightLineOnHover, }) => { const [t] = useTranslation("repos"); const [collapsed, setCollapsed] = useState(true); const [file, setFile] = useState(fileProp); const [sideBySide, setSideBySide] = useState(sideBySideProp); const [whitespace, setWhitespace] = useState(!!whitespaceProp); const diffExpander = useMemo(() => new DiffExpander(file), [file]); const [expansionError, setExpansionError] = useState(); const viewType = useMemo(() => (sideBySide ? "split" : "unified"), [sideBySide]); const hasContent = useMemo(() => determineHasContent(file), [file]); const newFileLink = (file._links?.newFile as Link)?.href; const oldFileLink = (file._links?.oldFile as Link)?.href; const { data: newContentType } = useContentType(newFileLink, { enabled: !hasContent && !!newFileLink }); const { data: oldContentType } = useContentType(oldFileLink, { enabled: !hasContent && !!oldFileLink }); const canRenderContent = useMemo( () => hasContent || (["add", "modify", "delete"].includes(file.type) && canDisplayBinaryFile(oldContentType, newContentType)), [file, hasContent, newContentType, oldContentType] ); const canRenderSideBySide = useMemo(() => hasContent && file.type === "modify", [file, hasContent]); const isCollapsed = useMemo(() => { if (isCollapsedProp) { return isCollapsedProp(fileProp); } return collapsed; }, [collapsed, fileProp, isCollapsedProp]); useEffect(() => { if (!isCollapsedProp) { let defaultCollapse = !hasContent; if (typeof defaultCollapseProp === "boolean") { defaultCollapse = defaultCollapseProp; } else if (typeof defaultCollapseProp === "function") { defaultCollapse = defaultCollapseProp(file.oldPath, file.newPath); } setCollapsed(defaultCollapse); } }, [defaultCollapseProp, file, hasContent, isCollapsedProp]); const toggleCollapse = useCallback( (event: React.MouseEvent) => { if (canRenderContent) { if (onCollapseStateChange) { onCollapseStateChange(file); } else { setCollapsed((prev) => !prev); } } if (stickyHeader) { const element = document.getElementById(event.currentTarget.id); // Prevent skipping diffs on collapsing long ones because of the sticky header // We jump to the start of the diff and afterwards go slightly up to show the diff header right under the page header // Only scroll if diff is not collapsed and is using the "sticky" mode const pageHeaderSize = 50; if ( element && (isCollapsedProp ? !isCollapsedProp(file) : !collapsed) && element.getBoundingClientRect().top < pageHeaderSize ) { element.scrollIntoView(); window.scrollBy(0, -pageHeaderSize); } } }, [collapsed, file, canRenderContent, isCollapsedProp, onCollapseStateChange, stickyHeader] ); const toggleSideBySide = useCallback((callback: () => void) => { setSideBySide((prev) => !prev); callback(); }, []); const toggleWhiteSpace = useCallback( (callback: () => void) => { setWhitespace(!whitespace); callback(); }, [whitespace] ); const sideBySideToggle = useMemo( () => canRenderSideBySide && ( {({ setCollapsed }) => ( toggleSideBySide(() => { if (sideBySide) { setCollapsed(true); } }) } /> )} ), [canRenderSideBySide, sideBySide, t, toggleSideBySide] ); const whitespaceToggle = useMemo( () => hasContent && ( {({ setCollapsed }) => ( toggleWhiteSpace(() => { if (whitespace) { setCollapsed(true); } }) } /> )} ), [hasContent, t, toggleWhiteSpace, whitespace] ); const errorModal = useMemo( () => expansionError ? ( setExpansionError(undefined)} body={} active={true} /> ) : null, [expansionError, t] ); const innerContent = useMemo( () => (
{fileAnnotationFactory ? fileAnnotationFactory(file) : null} {hasContent ? ( {(hunks: HunkType[]) => hunks?.map((hunk, n) => ( )) } ) : ( )}
), [ annotationFactory, diffExpander, file, fileAnnotationFactory, hasContent, highlightLineOnHover, hunkClass, hunkGutterHoverIcon, markConflicts, newContentType, newFileLink, oldContentType, oldFileLink, onClick, sideBySide, whitespace, viewType, ] ); const body = useMemo(() => (!isCollapsed ? innerContent : null), [innerContent, isCollapsed]); const openInFullscreen = useMemo( () => canRenderContent ? ( {innerContent}} /> ) : null, [canRenderContent, file, innerContent] ); const collapseIcon = useMemo( () => isCollapsed ? ( angle-right ) : ( angle-down ), [isCollapsed, t] ); return ( {errorModal}
{canRenderContent ? collapseIcon : null}
{sideBySideToggle} {openInFullscreen} {whitespaceToggle} {fileControlFactory ? fileControlFactory(file, setCollapsed) : null}
{body}
); }; export default DiffFile;