implemented api for diff annotations

This commit is contained in:
Sebastian Sdorra
2019-02-27 11:56:50 +01:00
parent 3ac47b0977
commit 891e3587b3
8 changed files with 280 additions and 44 deletions

View File

@@ -1,32 +1,27 @@
//@flow
import React from "react";
import DiffFile from "./DiffFile";
import type { DiffObjectProps } from "./DiffTypes";
type Props = {
diff: any,
sideBySide: boolean
type Props = DiffObjectProps & {
diff: any
};
class Diff extends React.Component<Props> {
static defaultProps = {
sideBySide: false
};
renderFile = (file: any, i: number) => {
const { sideBySide } = this.props;
return <DiffFile key={i} file={file} sideBySide={sideBySide} />;
};
render() {
const { diff } = this.props;
const { diff, ...fileProps } = this.props;
return (
<>
{diff.map(this.renderFile)}
{diff.map((file, index) => (
<DiffFile key={index} file={file} {...fileProps} />
))}
</>
);
}
}
export default Diff;

View File

@@ -1,9 +1,16 @@
//@flow
import React from "react";
import { Hunk, Diff as DiffComponent } from "react-diff-view";
import {
Hunk,
Diff as DiffComponent,
getChangeKey,
Change,
DiffObjectProps,
File
} from "react-diff-view";
import injectSheets from "react-jss";
import classNames from "classnames";
import {translate} from "react-i18next";
import { translate } from "react-i18next";
const styles = {
panel: {
@@ -21,20 +28,18 @@ const styles = {
}
};
type Props = {
file: any,
sideBySide: boolean,
type Props = DiffObjectProps & {
file: File,
// context props
classes: any,
t: string => string
}
};
type State = {
collapsed: boolean
}
};
class DiffFile extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
@@ -43,23 +48,77 @@ class DiffFile extends React.Component<Props, State> {
}
toggleCollapse = () => {
this.setState((state) => ({
collapsed: ! state.collapsed
this.setState(state => ({
collapsed: !state.collapsed
}));
};
renderHunk = (hunk: any, i: number) => {
createHunkHeader = (hunk: Hunk, i: number) => {
const { classes } = this.props;
let header = null;
if (i > 0) {
header = <hr className={classes.hunkDivider} />;
return <hr className={classes.hunkDivider} />;
}
return <Hunk key={hunk.content} hunk={hunk} header={header} />;
return null;
};
collectHunkAnnotations = (hunk: Hunk) => {
const { annotationFactory, file } = this.props;
if (annotationFactory) {
return annotationFactory({
hunk,
file
});
}
};
handleClickEvent = (change: Change, hunk: Hunk) => {
const { file, onClick } = this.props;
const context = {
changeId: getChangeKey(change),
change,
hunk,
file
};
if (onClick) {
onClick(context);
}
};
createCustomEvents = (hunk: Hunk) => {
const { onClick } = this.props;
if (onClick) {
return {
gutter: {
onClick: (change: Change) => {
this.handleClickEvent(change, hunk);
}
}
};
}
};
renderHunk = (hunk: Hunk, i: number) => {
return (
<Hunk
key={hunk.content}
hunk={hunk}
header={this.createHunkHeader(hunk, i)}
widgets={this.collectHunkAnnotations(hunk)}
customEvents={this.createCustomEvents(hunk)}
/>
);
};
renderFileTitle = (file: any) => {
if (file.oldPath !== file.newPath && (file.type === "copy" || file.type === "rename")) {
return (<>{file.oldPath} <i className="fa fa-arrow-right" /> {file.newPath}</>);
if (
file.oldPath !== file.newPath &&
(file.type === "copy" || file.type === "rename")
) {
return (
<>
{file.oldPath} <i className="fa fa-arrow-right" /> {file.newPath}
</>
);
} else if (file.type === "delete") {
return file.oldPath;
}
@@ -73,11 +132,7 @@ class DiffFile extends React.Component<Props, State> {
if (key === value) {
value = file.type;
}
return (
<span className="tag is-info has-text-weight-normal">
{value}
</span>
);
return <span className="tag is-info has-text-weight-normal">{value}</span>;
};
render() {
@@ -92,7 +147,7 @@ class DiffFile extends React.Component<Props, State> {
body = (
<div className="panel-block is-paddingless is-size-7">
<DiffComponent viewType={viewType}>
{ file.hunks.map(this.renderHunk) }
{file.hunks.map(this.renderHunk)}
</DiffComponent>
</div>
);
@@ -100,21 +155,24 @@ class DiffFile extends React.Component<Props, State> {
return (
<div className={classNames("panel", classes.panel)}>
<div className={classNames("panel-heading", classes.header)} onClick={this.toggleCollapse}>
<div
className={classNames("panel-heading", classes.header)}
onClick={this.toggleCollapse}
>
<div className="level">
<div className="level-left">
<i className={icon} /><span className={classes.title}>{this.renderFileTitle(file)}</span>
</div>
<div className="level-right">
{this.renderChangeTag(file)}
<i className={icon} />
<span className={classes.title}>
{this.renderFileTitle(file)}
</span>
</div>
<div className="level-right">{this.renderChangeTag(file)}</div>
</div>
</div>
{body}
</div>
);
}
}
export default injectSheets(styles)(translate("repos")(DiffFile));

View File

@@ -0,0 +1,61 @@
// @flow
// We place the types here and not in @scm-manager/ui-types,
// because they represent not a real scm-manager related type.
// This types represents only the required types for the Diff related components,
// such as every other component does with its Props.
export type FileChangeType = "add" | "modify" | "delete" | "copy" | "rename";
export type File = {
hunks: Hunk[],
newEndingNewLine: boolean,
newMode?: string,
newPath: string,
newRevision?: string,
oldEndingNewLine: boolean,
oldMode?: string,
oldPath: string,
oldRevision?: string,
type: FileChangeType
};
export type Hunk = {
changes: Change[],
content: string
};
export type Change = {
content: string,
isNormal: boolean,
newLineNumber: number,
oldLineNumber: number,
type: string
};
export type BaseContext = {
hunk: Hunk,
file: File
};
export type AnnotationFactoryContext = BaseContext;
// key = change id, value = react component
export type AnnotationFactory = (
context: AnnotationFactoryContext
) => {
[string]: any
};
export type DiffEventContext = BaseContext & {
changeId: string,
change: Change
};
export type DiffEventHandler = (context: DiffEventContext) => void;
export type DiffObjectProps = {
sideBySide: boolean,
onClick?: DiffEventHandler,
annotationFactory?: AnnotationFactory
};

View File

@@ -6,10 +6,10 @@ import parser from "gitdiff-parser";
import Loading from "../Loading";
import Diff from "./Diff";
import type {DiffObjectProps} from "./DiffTypes";
type Props = {
url: string,
sideBySide: boolean
type Props = DiffObjectProps & {
url: string
};
type State = {
@@ -71,7 +71,7 @@ class LoadingDiff extends React.Component<Props, State> {
return null;
}
else {
return <Diff diff={diff} />;
return <Diff diff={diff} {...this.props} />;
}
}

View File

@@ -0,0 +1,18 @@
// @flow
import type { BaseContext, File, Hunk } from "./DiffTypes";
export function getPath(file: File) {
if (file.type === "delete") {
return file.oldPath;
}
return file.newPath;
}
export function createHunkIdentifier(file: File, hunk: Hunk) {
const path = getPath(file);
return `${file.type}_${path}_${hunk.content}`;
}
export function createHunkIdentifierFromContext(ctx: BaseContext) {
return createHunkIdentifier(ctx.file, ctx.hunk);
}

View File

@@ -0,0 +1,76 @@
// @flow
import type { File, FileChangeType, Hunk } from "./DiffTypes";
import {
getPath,
createHunkIdentifier,
createHunkIdentifierFromContext
} from "./diffs";
describe("tests for diff util functions", () => {
const file = (
type: FileChangeType,
oldPath: string,
newPath: string
): File => {
return {
hunks: [],
type: type,
oldPath,
newPath,
newEndingNewLine: true,
oldEndingNewLine: true
};
};
const add = (path: string) => {
return file("add", "/dev/null", path);
};
const rm = (path: string) => {
return file("delete", path, "/dev/null");
};
const modify = (path: string) => {
return file("modify", path, path);
};
const createHunk = (content: string): Hunk => {
return {
content,
changes: []
};
};
describe("getPath tests", () => {
it("should pick the new path, for type add", () => {
const file = add("/etc/passwd");
const path = getPath(file);
expect(path).toBe("/etc/passwd");
});
it("should pick the old path, for type delete", () => {
const file = rm("/etc/passwd");
const path = getPath(file);
expect(path).toBe("/etc/passwd");
});
});
describe("createHunkIdentifier tests", () => {
it("should create identifier", () => {
const file = modify("/etc/passwd");
const hunk = createHunk("@@ -1,18 +1,15 @@");
const identifier = createHunkIdentifier(file, hunk);
expect(identifier).toBe("modify_/etc/passwd_@@ -1,18 +1,15 @@");
});
});
describe("createHunkIdentifierFromContext tests", () => {
it("should create identifier", () => {
const identifier = createHunkIdentifierFromContext({
file: rm("/etc/passwd"),
hunk: createHunk("@@ -1,42 +1,39 @@")
});
expect(identifier).toBe("delete_/etc/passwd_@@ -1,42 +1,39 @@");
});
});
});

View File

@@ -1,5 +1,20 @@
// @flow
import * as diffs from "./diffs";
export { diffs };
export * from "./changesets";
export { default as Diff } from "./Diff";
export { default as LoadingDiff } from "./LoadingDiff";
export type {
File,
FileChangeType,
Hunk,
Change,
BaseContext,
AnnotationFactory,
AnnotationFactoryContext,
DiffEventHandler,
DiffEventContext
} from "./DiffTypes";