mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-02 19:45:51 +01:00
refactor to use file control factory
This commit is contained in:
@@ -29,13 +29,15 @@ import parser from "gitdiff-parser";
|
||||
import simpleDiff from "../__resources__/Diff.simple";
|
||||
import hunksDiff from "../__resources__/Diff.hunks";
|
||||
import binaryDiff from "../__resources__/Diff.binary";
|
||||
import { DiffEventContext, File } from "./DiffTypes";
|
||||
import {DiffEventContext, File, FileControlFactory} from "./DiffTypes";
|
||||
import Toast from "../toast/Toast";
|
||||
import { getPath } from "./diffs";
|
||||
import DiffButton from "./DiffButton";
|
||||
import styled from "styled-components";
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
import {one} from "../__resources__/changesets";
|
||||
import {one, two} from "../__resources__/changesets";
|
||||
import {Changeset} from "@scm-manager/ui-types";
|
||||
import JumpToFileButton from "./JumpToFileButton";
|
||||
|
||||
const diffFiles = parser.parse(simpleDiff);
|
||||
|
||||
@@ -45,16 +47,50 @@ const Container = styled.div`
|
||||
|
||||
const RoutingDecorator = (story: () => ReactNode) => <MemoryRouter initialEntries={["/"]}>{story()}</MemoryRouter>;
|
||||
|
||||
const fileControlFactory: (changeset: Changeset) => FileControlFactory = (changeset) => (file) => {
|
||||
const baseUrl = "/repo/hitchhiker/heartOfGold/code/changeset";
|
||||
const sourceLink = {
|
||||
url: `${baseUrl}/${changeset.id}/${file.newPath}/`,
|
||||
label: "Jump to source"
|
||||
};
|
||||
const targetLink = changeset._embedded?.parents?.length === 1 && {
|
||||
url: `${baseUrl}/${changeset._embedded.parents[0].id}/${file.oldPath}`,
|
||||
label: "Jump to target"
|
||||
};
|
||||
|
||||
const links = [];
|
||||
switch (file.type) {
|
||||
case "add":
|
||||
links.push(sourceLink);
|
||||
break;
|
||||
case "delete":
|
||||
if (targetLink) {
|
||||
links.push(targetLink);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (targetLink) {
|
||||
links.push(sourceLink, targetLink);
|
||||
} else {
|
||||
links.push(sourceLink);
|
||||
}
|
||||
}
|
||||
|
||||
return links.map(({url, label}) => <JumpToFileButton
|
||||
tooltip={label}
|
||||
link={url}
|
||||
/>);
|
||||
};
|
||||
|
||||
storiesOf("Diff", module)
|
||||
.addDecorator(RoutingDecorator)
|
||||
.addDecorator(storyFn => <Container>{storyFn()}</Container>)
|
||||
.add("Default", () => <Diff diff={diffFiles} changeset={one} />)
|
||||
.add("Side-By-Side", () => <Diff diff={diffFiles} sideBySide={true} changeset={one} />)
|
||||
.add("Collapsed", () => <Diff diff={diffFiles} defaultCollapse={true} changeset={one} />)
|
||||
.add("Default", () => <Diff diff={diffFiles} />)
|
||||
.add("Side-By-Side", () => <Diff diff={diffFiles} sideBySide={true} />)
|
||||
.add("Collapsed", () => <Diff diff={diffFiles} defaultCollapse={true} fileControlFactory={fileControlFactory(two)} />)
|
||||
.add("File Controls", () => (
|
||||
<Diff
|
||||
diff={diffFiles}
|
||||
changeset={one}
|
||||
fileControlFactory={() => (
|
||||
<DiffButton
|
||||
tooltip="A skull and crossbones or death's head is a symbol consisting of a human skull and two long bones crossed together under or behind the skull. The design originates in the Late Middle Ages as a symbol of death and especially as a memento mori on tombstones."
|
||||
@@ -67,14 +103,12 @@ storiesOf("Diff", module)
|
||||
.add("File Annotation", () => (
|
||||
<Diff
|
||||
diff={diffFiles}
|
||||
changeset={one}
|
||||
fileAnnotationFactory={file => [<p key={file.newPath}>Custom File annotation for {file.newPath}</p>]}
|
||||
/>
|
||||
))
|
||||
.add("Line Annotation", () => (
|
||||
<Diff
|
||||
diff={diffFiles}
|
||||
changeset={one}
|
||||
annotationFactory={ctx => {
|
||||
return {
|
||||
N2: <p key="N2">Line Annotation</p>
|
||||
@@ -94,7 +128,7 @@ storiesOf("Diff", module)
|
||||
return (
|
||||
<>
|
||||
{changeId && <Toast type="info" title={"Change " + changeId} />}
|
||||
<Diff diff={diffFiles} changeset={one} onClick={onClick} />
|
||||
<Diff diff={diffFiles} onClick={onClick} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -102,11 +136,11 @@ storiesOf("Diff", module)
|
||||
})
|
||||
.add("Hunks", () => {
|
||||
const hunkDiffFiles = parser.parse(hunksDiff);
|
||||
return <Diff diff={hunkDiffFiles} changeset={one} />;
|
||||
return <Diff diff={hunkDiffFiles} />;
|
||||
})
|
||||
.add("Binaries", () => {
|
||||
const binaryDiffFiles = parser.parse(binaryDiff);
|
||||
return <Diff diff={binaryDiffFiles} changeset={one} />;
|
||||
return <Diff diff={binaryDiffFiles} />;
|
||||
})
|
||||
.add("SyntaxHighlighting", () => {
|
||||
const filesWithLanguage = diffFiles.map((file: File) => {
|
||||
@@ -118,22 +152,20 @@ storiesOf("Diff", module)
|
||||
}
|
||||
return file;
|
||||
});
|
||||
return <Diff diff={filesWithLanguage} changeset={one} />;
|
||||
return <Diff diff={filesWithLanguage} />;
|
||||
})
|
||||
.add("CollapsingWithFunction", () => (
|
||||
<Diff diff={diffFiles} changeset={one} defaultCollapse={(oldPath, newPath) => oldPath.endsWith(".java")} />
|
||||
<Diff diff={diffFiles} defaultCollapse={(oldPath, newPath) => oldPath.endsWith(".java")} />
|
||||
))
|
||||
.add("Expandable", () => {
|
||||
const filesWithLanguage = diffFiles.map((file: File) => {
|
||||
file._links = { lines: { href: "http://example.com/" } };
|
||||
return file;
|
||||
});
|
||||
return <Diff diff={filesWithLanguage} changeset={one} />;
|
||||
return <Diff diff={filesWithLanguage} />;
|
||||
})
|
||||
.add("WithLinkToFile", () => (
|
||||
<Diff
|
||||
diff={diffFiles}
|
||||
changeset={one}
|
||||
baseUrl="/repo/hitchhiker/heartOfGold/code/changeset"
|
||||
/>
|
||||
));
|
||||
|
||||
@@ -23,16 +23,14 @@
|
||||
*/
|
||||
import React from "react";
|
||||
import DiffFile from "./DiffFile";
|
||||
import { DiffObjectProps, File } from "./DiffTypes";
|
||||
import {DiffObjectProps, File, FileControlFactory} from "./DiffTypes";
|
||||
import Notification from "../Notification";
|
||||
import {WithTranslation, withTranslation} from "react-i18next";
|
||||
import {Changeset} from "@scm-manager/ui-types";
|
||||
|
||||
type Props = WithTranslation &
|
||||
DiffObjectProps & {
|
||||
diff: File[];
|
||||
changeset: Changeset;
|
||||
baseUrl?: string;
|
||||
fileControlFactory?: FileControlFactory;
|
||||
};
|
||||
|
||||
class Diff extends React.Component<Props> {
|
||||
|
||||
@@ -39,16 +39,12 @@ import HunkExpandLink from "./HunkExpandLink";
|
||||
import {Modal} from "../modals";
|
||||
import ErrorNotification from "../ErrorNotification";
|
||||
import HunkExpandDivider from "./HunkExpandDivider";
|
||||
import JumpToFileButton from "./JumpToFileButton";
|
||||
import {Changeset} from "@scm-manager/ui-types";
|
||||
|
||||
const EMPTY_ANNOTATION_FACTORY = {};
|
||||
|
||||
type Props = DiffObjectProps &
|
||||
WithTranslation & {
|
||||
file: File;
|
||||
changeset: Changeset;
|
||||
baseUrl?: string;
|
||||
};
|
||||
|
||||
type Collapsible = {
|
||||
@@ -395,7 +391,7 @@ class DiffFile extends React.Component<Props, State> {
|
||||
hasContent = (file: File) => file && !file.isBinary && file.hunks && file.hunks.length > 0;
|
||||
|
||||
render() {
|
||||
const { fileControlFactory, fileAnnotationFactory, changeset: { id: changesetId, _embedded: { parents } }, baseUrl, t } = this.props;
|
||||
const {fileControlFactory, fileAnnotationFactory, t} = this.props;
|
||||
const {file, collapsed, sideBySide, diffExpander, expansionError} = this.state;
|
||||
const viewType = sideBySide ? "split" : "unified";
|
||||
|
||||
@@ -419,31 +415,8 @@ class DiffFile extends React.Component<Props, State> {
|
||||
}
|
||||
const collapseIcon = this.hasContent(file) ? <Icon name={icon} color="inherit"/> : null;
|
||||
const fileControls = fileControlFactory ? fileControlFactory(file, this.setCollapse) : null;
|
||||
let jumpToSource = null;
|
||||
let jumpToTarget = null;
|
||||
if (changesetId && baseUrl) {
|
||||
const jumpToSourceButton = <JumpToFileButton
|
||||
tooltip={this.props.t("diff.jumpToSource")}
|
||||
link={`${baseUrl.substr(0, baseUrl.lastIndexOf("/"))}/sources/${changesetId}/${file.newPath}/`}
|
||||
/>;
|
||||
const jumpToTargetButton = parents?.length === 1 && <JumpToFileButton
|
||||
tooltip={this.props.t("diff.jumpToTarget")}
|
||||
link={`${baseUrl.substr(0, baseUrl.lastIndexOf("/"))}/sources/${parents[0].id}/${file.oldPath}/`}
|
||||
/>;
|
||||
switch (file.type) {
|
||||
case "add":
|
||||
jumpToSource = jumpToSourceButton;
|
||||
break;
|
||||
case "delete":
|
||||
jumpToTarget = jumpToTargetButton;
|
||||
break;
|
||||
default:
|
||||
jumpToSource = jumpToSourceButton;
|
||||
jumpToTarget = jumpToTargetButton;
|
||||
}
|
||||
}
|
||||
const sideBySideToggle =
|
||||
file.hunks && file.hunks.length > 0 ? (
|
||||
file.hunks && file.hunks.length > 0 && (
|
||||
<ButtonWrapper className={classNames("level-right", "is-flex")}>
|
||||
<ButtonGroup>
|
||||
<MenuContext.Consumer>
|
||||
@@ -462,15 +435,6 @@ class DiffFile extends React.Component<Props, State> {
|
||||
)}
|
||||
</MenuContext.Consumer>
|
||||
{fileControls}
|
||||
{jumpToSource}
|
||||
{jumpToTarget}
|
||||
</ButtonGroup>
|
||||
</ButtonWrapper>
|
||||
) : (
|
||||
<ButtonWrapper className={classNames("level-right", "is-flex")}>
|
||||
<ButtonGroup>
|
||||
{jumpToSource}
|
||||
{jumpToTarget}
|
||||
</ButtonGroup>
|
||||
</ButtonWrapper>
|
||||
);
|
||||
|
||||
@@ -33,13 +33,10 @@ import { DiffObjectProps, File } from "./DiffTypes";
|
||||
import {NotFoundError} from "../errors";
|
||||
import {Notification} from "../index";
|
||||
import {withTranslation, WithTranslation} from "react-i18next";
|
||||
import {Changeset} from "@scm-manager/ui-types";
|
||||
|
||||
type Props = WithTranslation &
|
||||
DiffObjectProps & {
|
||||
url: string;
|
||||
changeset: Changeset;
|
||||
baseUrl: string;
|
||||
};
|
||||
|
||||
type State = {
|
||||
|
||||
@@ -22,15 +22,16 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React from "react";
|
||||
import { Changeset, Link, Collection } from "@scm-manager/ui-types";
|
||||
import {Changeset, Collection, Link} from "@scm-manager/ui-types";
|
||||
import LoadingDiff from "../LoadingDiff";
|
||||
import Notification from "../../Notification";
|
||||
import {WithTranslation, withTranslation} from "react-i18next";
|
||||
import {FileControlFactory} from "../DiffTypes";
|
||||
|
||||
type Props = WithTranslation & {
|
||||
changeset: Changeset;
|
||||
baseUrl: string;
|
||||
defaultCollapse?: boolean;
|
||||
fileControlFactory?: FileControlFactory;
|
||||
};
|
||||
|
||||
export const isDiffSupported = (changeset: Collection) => {
|
||||
@@ -48,12 +49,12 @@ export const createUrl = (changeset: Collection) => {
|
||||
|
||||
class ChangesetDiff extends React.Component<Props> {
|
||||
render() {
|
||||
const { changeset, baseUrl, defaultCollapse, t } = this.props;
|
||||
const {changeset, fileControlFactory, defaultCollapse, t} = this.props;
|
||||
if (!isDiffSupported(changeset)) {
|
||||
return <Notification type="danger">{t("changeset.diffNotSupported")}</Notification>;
|
||||
} else {
|
||||
const url = createUrl(changeset);
|
||||
return <LoadingDiff url={url} defaultCollapse={defaultCollapse} sideBySide={false} changeset={changeset} baseUrl={baseUrl} />;
|
||||
return <LoadingDiff url={url} defaultCollapse={defaultCollapse} sideBySide={false} fileControlFactory={fileControlFactory}/>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ import ChangesetAuthor from "./ChangesetAuthor";
|
||||
import ChangesetTags from "./ChangesetTags";
|
||||
import ChangesetButtonGroup from "./ChangesetButtonGroup";
|
||||
import ChangesetDescription from "./ChangesetDescription";
|
||||
import {FileControlFactory} from "../DiffTypes";
|
||||
|
||||
type Props = WithTranslation & {
|
||||
repository: Repository;
|
||||
|
||||
@@ -45,11 +45,13 @@ export * from "./changesets";
|
||||
export { default as Diff } from "./Diff";
|
||||
export { default as DiffFile } from "./DiffFile";
|
||||
export { default as DiffButton } from "./DiffButton";
|
||||
export { FileControlFactory } from "./DiffTypes";
|
||||
export { default as LoadingDiff } from "./LoadingDiff";
|
||||
export { DefaultCollapsed, DefaultCollapsedFunction } from "./defaultCollapsed";
|
||||
export { default as RepositoryAvatar } from "./RepositoryAvatar";
|
||||
export { default as RepositoryEntry } from "./RepositoryEntry";
|
||||
export { default as RepositoryEntryLink } from "./RepositoryEntryLink";
|
||||
export { default as JumpToFileButton } from "./JumpToFileButton";
|
||||
|
||||
export {
|
||||
File,
|
||||
|
||||
@@ -224,7 +224,7 @@
|
||||
},
|
||||
"diff": {
|
||||
"jumpToSource": "Zur Quelldatei springen",
|
||||
"jumpToTarget": "Zur Zieldatei springen",
|
||||
"jumpToTarget": "Zur vorherigen Version der Datei springen",
|
||||
"sideBySide": "Zur zweispaltigen Ansicht wechseln",
|
||||
"combined": "Zur kombinierten Ansicht wechseln",
|
||||
"noDiffFound": "Kein Diff zwischen den ausgewählten Branches gefunden.",
|
||||
|
||||
@@ -231,7 +231,7 @@
|
||||
"copy": "copied"
|
||||
},
|
||||
"jumpToSource": "Jump to source file",
|
||||
"jumpToTarget": "Jump to target file",
|
||||
"jumpToTarget": "Jump to the previous version of the file",
|
||||
"sideBySide": "Switch to side-by-side view",
|
||||
"combined": "Switch to combined view",
|
||||
"noDiffFound": "No Diff between the selected branches found.",
|
||||
|
||||
@@ -43,11 +43,12 @@ import {
|
||||
} from "@scm-manager/ui-components";
|
||||
import ContributorTable from "./ContributorTable";
|
||||
import { Link as ReactLink } from "react-router-dom";
|
||||
import {FileControlFactory} from "@scm-manager/ui-components";
|
||||
|
||||
type Props = WithTranslation & {
|
||||
changeset: Changeset;
|
||||
repository: Repository;
|
||||
baseUrl: string;
|
||||
fileControlFactory?: FileControlFactory;
|
||||
};
|
||||
|
||||
type State = {
|
||||
@@ -131,6 +132,7 @@ const Contributors: FC<{ changeset: Changeset }> = ({ changeset }) => {
|
||||
</ContributorDetails>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContributorLine onClick={e => setOpen(!open)}>
|
||||
@@ -158,7 +160,7 @@ class ChangesetDetails extends React.Component<Props, State> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { changeset, repository, baseUrl, t } = this.props;
|
||||
const { changeset, repository, fileControlFactory, t } = this.props;
|
||||
const { collapsed } = this.state;
|
||||
|
||||
const description = changesets.parseDescription(changeset.description);
|
||||
@@ -239,7 +241,7 @@ class ChangesetDetails extends React.Component<Props, State> {
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ChangesetDiff changeset={changeset} baseUrl={baseUrl} defaultCollapse={collapsed} />
|
||||
<ChangesetDiff changeset={changeset} fileControlFactory={fileControlFactory} defaultCollapse={collapsed} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -35,12 +35,13 @@ import {
|
||||
isFetchChangesetPending
|
||||
} from "../modules/changesets";
|
||||
import ChangesetDetails from "../components/changesets/ChangesetDetails";
|
||||
import {FileControlFactory} from "@scm-manager/ui-components";
|
||||
|
||||
type Props = WithTranslation & {
|
||||
id: string;
|
||||
changeset: Changeset;
|
||||
repository: Repository;
|
||||
baseUrl: string;
|
||||
fileControlFactoryFactory?: (changeset: Changeset) => FileControlFactory;
|
||||
loading: boolean;
|
||||
error: Error;
|
||||
fetchChangesetIfNeeded: (repository: Repository, id: string) => void;
|
||||
@@ -61,7 +62,7 @@ class ChangesetView extends React.Component<Props> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { changeset, loading, error, t, repository, baseUrl } = this.props;
|
||||
const {changeset, loading, error, t, repository, fileControlFactoryFactory} = this.props;
|
||||
|
||||
if (error) {
|
||||
return <ErrorPage title={t("changesets.errorTitle")} subtitle={t("changesets.errorSubtitle")} error={error}/>;
|
||||
@@ -69,7 +70,8 @@ class ChangesetView extends React.Component<Props> {
|
||||
|
||||
if (!changeset || loading) return <Loading/>;
|
||||
|
||||
return <ChangesetDetails changeset={changeset} repository={repository} baseUrl={baseUrl} />;
|
||||
return <ChangesetDetails changeset={changeset} repository={repository}
|
||||
fileControlFactory={fileControlFactoryFactory && fileControlFactoryFactory(changeset)}/>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,21 +23,21 @@
|
||||
*/
|
||||
import React from "react";
|
||||
import {connect} from "react-redux";
|
||||
import { Redirect, Route, Switch, RouteComponentProps } from "react-router-dom";
|
||||
import {Redirect, Route, RouteComponentProps, Switch} from "react-router-dom";
|
||||
import {WithTranslation, withTranslation} from "react-i18next";
|
||||
import {binder, ExtensionPoint} from "@scm-manager/ui-extensions";
|
||||
import { Repository } from "@scm-manager/ui-types";
|
||||
import {Changeset, Repository} from "@scm-manager/ui-types";
|
||||
import {
|
||||
CustomQueryFlexWrappedColumns,
|
||||
ErrorPage,
|
||||
Loading,
|
||||
NavLink,
|
||||
Page,
|
||||
CustomQueryFlexWrappedColumns,
|
||||
PrimaryContentColumn,
|
||||
SecondaryNavigationColumn,
|
||||
SecondaryNavigation,
|
||||
SubNavigation,
|
||||
StateMenuContextProvider
|
||||
SecondaryNavigationColumn,
|
||||
StateMenuContextProvider,
|
||||
SubNavigation
|
||||
} from "@scm-manager/ui-components";
|
||||
import {fetchRepoByName, getFetchRepoFailure, getRepository, isFetchRepoPending} from "../modules/repos";
|
||||
import RepositoryDetails from "../components/RepositoryDetails";
|
||||
@@ -53,6 +53,7 @@ import { getLinks, getRepositoriesLink } from "../../modules/indexResource";
|
||||
import CodeOverview from "../codeSection/containers/CodeOverview";
|
||||
import ChangesetView from "./ChangesetView";
|
||||
import SourceExtensions from "../sources/containers/SourceExtensions";
|
||||
import {FileControlFactory, JumpToFileButton} from "@scm-manager/ui-components";
|
||||
|
||||
type Props = RouteComponentProps &
|
||||
WithTranslation & {
|
||||
@@ -117,7 +118,7 @@ class RepositoryRoot extends React.Component<Props> {
|
||||
|
||||
evaluateDestinationForCodeLink = () => {
|
||||
const {repository} = this.props;
|
||||
let url = `${this.matchedUrl()}/code`;
|
||||
const url = `${this.matchedUrl()}/code`;
|
||||
if (repository?._links?.sources) {
|
||||
return `${url}/sources/`;
|
||||
}
|
||||
@@ -153,6 +154,41 @@ class RepositoryRoot extends React.Component<Props> {
|
||||
redirectedUrl = url + "/info";
|
||||
}
|
||||
|
||||
const fileControlFactoryFactory: (changeset: Changeset) => FileControlFactory = (changeset) => (file) => {
|
||||
const baseUrl = `${url}/code/sources`;
|
||||
const sourceLink = {
|
||||
url: `${baseUrl}/${changeset.id}/${file.newPath}/`,
|
||||
label: t("diff.jumpToSource")
|
||||
};
|
||||
const targetLink = changeset._embedded?.parents?.length === 1 && {
|
||||
url: `${baseUrl}/${changeset._embedded.parents[0].id}/${file.oldPath}`,
|
||||
label: t("diff.jumpToTarget")
|
||||
};
|
||||
|
||||
const links = [];
|
||||
switch (file.type) {
|
||||
case "add":
|
||||
links.push(sourceLink);
|
||||
break;
|
||||
case "delete":
|
||||
if (targetLink) {
|
||||
links.push(targetLink);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (targetLink) {
|
||||
links.push(sourceLink, targetLink);
|
||||
} else {
|
||||
links.push(sourceLink);
|
||||
}
|
||||
}
|
||||
|
||||
return links.map(({url, label}) => <JumpToFileButton
|
||||
tooltip={label}
|
||||
link={url}
|
||||
/>);
|
||||
};
|
||||
|
||||
return (
|
||||
<StateMenuContextProvider>
|
||||
<Page
|
||||
@@ -182,7 +218,8 @@ class RepositoryRoot extends React.Component<Props> {
|
||||
<Route
|
||||
exact
|
||||
path={`${url}/code/changeset/:id`}
|
||||
render={() => <ChangesetView repository={repository} baseUrl={`${url}/code/changeset`} />}
|
||||
render={() => <ChangesetView repository={repository}
|
||||
fileControlFactoryFactory={fileControlFactoryFactory}/>}
|
||||
/>
|
||||
<Route
|
||||
path={`${url}/code/sourceext/:extension`}
|
||||
|
||||
Reference in New Issue
Block a user