Use different icons in file tree to show status (#2246)

---------

Co-authored-by: Florian Scholdei <florian.scholdei@cloudogu.com>
This commit is contained in:
Lukas
2025-04-08 08:10:19 +02:00
committed by GitHub
parent 22e2fc0a86
commit e6a1557fff
7 changed files with 136 additions and 70 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 867 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 855 KiB

View File

@@ -0,0 +1,2 @@
- type: added
description: Colored status icons in file tree

View File

@@ -28,10 +28,10 @@ const LayoutRadioButtons = ({ setLayout, layout }: LayoutProps) => {
const [t] = useTranslation("repos");
return (
<Field as="fieldset" className={"pl-4"}>
<Field as="fieldset">
<RadioGroup
aria-label={t("changesets.diffTree.layout")}
className="mt-2 columns is is-multiline is-align-items-center"
className="is-flex is-flex-wrap-wrap is-align-items-center mt-2"
value={layout}
onValueChange={(value) => setLayout(value as unknown as LayoutMode)}
>

View File

@@ -16,6 +16,7 @@
import React, { FC, useState } from "react";
import { useTranslation } from "react-i18next";
import classNames from "classnames";
import { NotFoundError, useDiff } from "@scm-manager/ui-api";
import ErrorNotification from "../ErrorNotification";
import Loading from "../Loading";
@@ -26,8 +27,8 @@ import { DiffObjectProps } from "./DiffTypes";
import DiffStatistics from "./DiffStatistics";
import { DiffDropDown } from "../index";
import DiffFileTree from "./diff/DiffFileTree";
import { DiffContent, Divider, FileTreeContent, StickyFileDiffContainer } from "./diff/styledElements";
import { useHistory, useLocation } from "react-router-dom";
import { DiffContent, DiffTreeTitle, FileTreeContent, StickyFileDiffContainer } from "./diff/styledElements";
import { useLocation } from "react-router-dom";
import { getFileNameFromHash } from "./diffs";
import LayoutRadioButtons from "./LayoutRadioButtons";
import { useAriaId } from "@scm-manager/ui-core";
@@ -66,7 +67,6 @@ const LoadingDiff: FC<Props> = ({ url, limit, refetchOnWindowFocus, ...props })
const [prevHash, setPrevHash] = useState("");
const diffContentId = useAriaId();
const location = useLocation();
const history = useHistory();
const { error, isLoading, data, fetchNextPage, isFetchingNextPage } = useDiff(url, {
limit,
@@ -79,10 +79,9 @@ const LoadingDiff: FC<Props> = ({ url, limit, refetchOnWindowFocus, ...props })
setCollapsed(!collapsed);
};
const setFilePath = (path: string) => {
const setFilePath = () => {
setPrevHash("");
setLayout("Both");
history.push(`#diff-${encodeURIComponent(path)}`);
};
if (error) {
@@ -97,7 +96,7 @@ const LoadingDiff: FC<Props> = ({ url, limit, refetchOnWindowFocus, ...props })
} else {
return (
<>
<Divider />
<hr />
<div className="is-flex is-justify-content-space-between">
<DiffStatistics data={data.statistics} />
<DiffDropDown
@@ -110,22 +109,23 @@ const LoadingDiff: FC<Props> = ({ url, limit, refetchOnWindowFocus, ...props })
/>
</div>
<LayoutRadioButtons layout={layout} setLayout={setLayout} />
<div className="is-flex mb-4 mt-1 columns is-multiline">
<div className="is-flex columns is-multiline mb-4 mt-1 ">
<StickyFileDiffContainer
className={
(layout === "Both" ? "column pl-3 is-one-quarter" : "column pl-3 is-full") +
(layout !== "Diff" ? "" : " is-hidden")
}
className={classNames(
"column",
"pt-0",
layout === "Both" ? "is-one-quarter" : "is-full",
layout !== "Diff" ? "" : " is-hidden"
)}
>
<FileTreeContent className={"p-3"} isBorder={layout !== "Diff"}>
<h3 className={"title is-6 pt-4"}>{t("changesets.diffTree.title")}</h3>
<Divider />
<FileTreeContent className="p-0" isBorder={layout !== "Diff"}>
<DiffTreeTitle className="title is-6 m-0 p-4">{t("changesets.diffTree.title")}</DiffTreeTitle>
{data?.tree && (
<DiffFileTree
tree={data.tree}
currentFile={decodeURIComponent(getFileNameFromHash(location.hash) ?? "")}
setCurrentFile={setFilePath}
gap={12}
gap={10}
/>
)}
</FileTreeContent>

View File

@@ -14,39 +14,35 @@
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
import { FileTree } from "@scm-manager/ui-types";
import React, { FC } from "react";
import { FileDiffContainer, FileDiffContent } from "./styledElements";
import { Icon } from "@scm-manager/ui-core";
import { Link } from "react-router-dom";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import classNames from "classnames";
import { FileTree } from "@scm-manager/ui-types";
import { FileDiffContent, StackedSpan, StyledIcon } from "./styledElements";
type Props = { tree: FileTree; currentFile: string; setCurrentFile: (path: string) => void; gap?: number };
const StyledIcon = styled(Icon)`
min-width: 1.5rem;
`;
const DiffFileTree: FC<Props> = ({ tree, currentFile, setCurrentFile, gap = 15 }) => {
return (
<FileDiffContainer className={"mt-4 py-3 pr-2"}>
<FileDiffContent gap={gap}>
{Object.keys(tree.children).map((key) => (
<TreeNode
key={key}
node={tree.children[key]}
parentPath={""}
currentFile={currentFile}
setCurrentFile={setCurrentFile}
/>
))}
</FileDiffContent>
</FileDiffContainer>
<FileDiffContent gap={gap}>
{Object.keys(tree.children).map((key) => (
<TreeNode
key={key}
node={tree.children[key]}
parentPath=""
currentFile={currentFile}
setCurrentFile={setCurrentFile}
/>
))}
</FileDiffContent>
);
};
export default DiffFileTree;
type ChangeType = "add" | "modify" | "delete" | "rename" | "copy";
type NodeProps = { node: FileTree; parentPath: string; currentFile: string; setCurrentFile: (path: string) => void };
const addPath = (parentPath: string, path: string) => {
@@ -62,10 +58,10 @@ const TreeNode: FC<NodeProps> = ({ node, parentPath, currentFile, setCurrentFile
return (
<li>
{Object.keys(node.children).length > 0 ? (
<ul className={"py-1 pr-1 pl-3"}>
<li className={"is-flex has-text-grey"}>
<ul className="py-1 pl-3">
<li className="is-flex has-text-grey">
<StyledIcon alt={t("diff.showContent")}>folder</StyledIcon>
<div className={"ml-1"}>{node.nodeName}</div>
<div className="ml-1">{node.nodeName}</div>
</li>
{Object.keys(node.children).map((key) => (
<TreeNode
@@ -79,6 +75,7 @@ const TreeNode: FC<NodeProps> = ({ node, parentPath, currentFile, setCurrentFile
</ul>
) : (
<TreeFile
changeType={node.changeType.toLowerCase() as ChangeType}
path={node.nodeName}
parentPath={parentPath}
currentFile={currentFile}
@@ -89,13 +86,41 @@ const TreeNode: FC<NodeProps> = ({ node, parentPath, currentFile, setCurrentFile
);
};
type FileProps = { path: string; parentPath: string; currentFile: string; setCurrentFile: (path: string) => void };
const getColor = (changeType: ChangeType) => {
switch (changeType) {
case "add":
return "success";
case "modify":
case "rename":
case "copy":
return "info";
case "delete":
return "danger";
}
};
export const TreeFileContent = styled.div`
cursor: pointer;
`;
const getIcon = (changeType: ChangeType) => {
switch (changeType) {
case "add":
case "copy":
return "plus";
case "modify":
case "rename":
return "circle";
case "delete":
return "minus";
}
};
const TreeFile: FC<FileProps> = ({ path, parentPath, currentFile, setCurrentFile }) => {
type FileProps = {
changeType: ChangeType;
path: string;
parentPath: string;
currentFile: string;
setCurrentFile: (path: string) => void;
};
const TreeFile: FC<FileProps> = ({ changeType, path, parentPath, currentFile, setCurrentFile }) => {
const [t] = useTranslation("repos");
const completePath = addPath(parentPath, path);
@@ -104,17 +129,34 @@ const TreeFile: FC<FileProps> = ({ path, parentPath, currentFile, setCurrentFile
};
return (
<TreeFileContent className={"is-flex py-1 pl-3"} onClick={() => setCurrentFile(completePath)}>
{isCurrentFile() ? (
<StyledIcon style={{ minWidth: "1.5rem" }} key={completePath + "file"} alt={t("diff.showContent")}>
<Link
className="is-flex py-1 pl-3 has-cursor-pointer"
onClick={() => setCurrentFile(completePath)}
to={`#diff-${encodeURIComponent(completePath)}`}
>
<StackedSpan className="fa-stack">
<StyledIcon
className={classNames("fa-stack-2x", `has-text-${getColor(changeType)}`)}
key={completePath + "file"}
type={isCurrentFile() ? "fas" : "far"}
alt={t("diff.showContent")}
>
file
</StyledIcon>
) : (
<StyledIcon style={{ minWidth: "1.5rem" }} key={completePath + "file"} type="far" alt={t("diff.showContent")}>
file
<StyledIcon
className={classNames(
"fa-stack-1x",
"is-relative",
isCurrentFile() ? "has-text-secondary-least" : `has-text-${getColor(changeType)}`
)}
isSmaller={getIcon(changeType) === "circle"}
key={changeType}
alt={t(`diff.changes.${changeType}`)}
>
{getIcon(changeType)}
</StyledIcon>
)}
<div className={"ml-1"}>{path}</div>
</TreeFileContent>
</StackedSpan>
<div className="ml-1">{path}</div>
</Link>
);
};

View File

@@ -17,6 +17,8 @@
import styled from "styled-components";
// @ts-ignore react-diff-view does not provide types
import { Hunk } from "react-diff-view";
import { Icon } from "@scm-manager/ui-core";
import { devices } from "../../devices";
export type Collapsible = {
collapsed?: boolean;
@@ -83,27 +85,37 @@ export const PanelHeading = styled.div<{ sticky?: boolean | number }>`
}}
`;
export const StickyFileDiffContainer = styled.div`
top: 5rem;
position: sticky;
height: 100%;
@media screen and (max-width: ${devices.mobile.width}px) {
top: 0;
position: relative;
flex: 0 0 100%;
margin-bottom: 0.5rem;
}
`;
export const FileTreeContent = styled.div<{ isBorder: boolean }>`
${({ isBorder }) =>
isBorder &&
`
border: 1px solid var(--scm-border-color);
border-radius: 1rem;
border-radius: 0.25rem;
overflow: hidden;
`}
`;
export const DiffTreeTitle = styled.h3`
border-bottom: 1px solid var(--scm-border-color);
box-shadow: 0 24px 3px -24px var(--scm-border-color);
`;
export const DiffContent = styled.div`
width: 100%;
`;
export const StickyFileDiffContainer = styled.div`
top: 3rem;
position: sticky;
height: 100%;
`;
export const FileDiffContainer = styled.div`
top: 5rem;
padding-top: 0;
`;
export const FileDiffContent = styled.ul<{ gap?: number }>`
@@ -120,8 +132,18 @@ export const FileDiffContent = styled.ul<{ gap?: number }>`
}};
`;
export const Divider = styled.div`
margin-bottom: 16px;
border-bottom: 1px solid var(--scm-border-color);
box-shadow: 0 24px 3px -24px var(--scm-border-color);
export const StackedSpan = styled.span`
width: 3em;
height: 3em;
font-size: 0.5em;
`;
export const StyledIcon = styled(Icon)<{ isSmaller?: boolean }>`
${({ isSmaller }) =>
isSmaller &&
`
font-size: 0.5em;
margin-top: 0.05rem;
`}
min-width: 1.5rem;
`;