mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-09 15:05:44 +01:00
merge with develop
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -114,4 +114,11 @@ storiesOf("Diff", module)
|
||||
})
|
||||
.add("CollapsingWithFunction", () => (
|
||||
<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} />;
|
||||
});
|
||||
|
||||
404
scm-ui/ui-components/src/repos/DiffExpander.test.ts
Normal file
404
scm-ui/ui-components/src/repos/DiffExpander.test.ts
Normal file
@@ -0,0 +1,404 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import fetchMock from "fetch-mock";
|
||||
import DiffExpander from "./DiffExpander";
|
||||
import { File, Hunk } from "./DiffTypes";
|
||||
|
||||
const HUNK_0: Hunk = {
|
||||
content: "@@ -1,8 +1,8 @@",
|
||||
oldStart: 1,
|
||||
newStart: 1,
|
||||
oldLines: 8,
|
||||
newLines: 8,
|
||||
changes: [
|
||||
{ content: "line", type: "normal", oldLineNumber: 1, newLineNumber: 1, isNormal: true },
|
||||
{ content: "line", type: "normal", oldLineNumber: 2, newLineNumber: 2, isNormal: true },
|
||||
{ content: "line", type: "delete", lineNumber: 3, isDelete: true },
|
||||
{ content: "line", type: "delete", lineNumber: 4, isDelete: true },
|
||||
{ content: "line", type: "delete", lineNumber: 5, isDelete: true },
|
||||
{ content: "line", type: "insert", lineNumber: 3, isInsert: true },
|
||||
{ content: "line", type: "insert", lineNumber: 4, isInsert: true },
|
||||
{ content: "line", type: "insert", lineNumber: 5, isInsert: true },
|
||||
{ content: "line", type: "normal", oldLineNumber: 6, newLineNumber: 6, isNormal: true },
|
||||
{ content: "line", type: "normal", oldLineNumber: 7, newLineNumber: 7, isNormal: true },
|
||||
{ content: "line", type: "normal", oldLineNumber: 8, newLineNumber: 8, isNormal: true }
|
||||
]
|
||||
};
|
||||
const HUNK_1: Hunk = {
|
||||
content: "@@ -14,6 +14,7 @@",
|
||||
oldStart: 14,
|
||||
newStart: 14,
|
||||
oldLines: 6,
|
||||
newLines: 7,
|
||||
changes: [
|
||||
{ content: "line", type: "normal", oldLineNumber: 14, newLineNumber: 14, isNormal: true },
|
||||
{ content: "line", type: "normal", oldLineNumber: 15, newLineNumber: 15, isNormal: true },
|
||||
{ content: "line", type: "normal", oldLineNumber: 16, newLineNumber: 16, isNormal: true },
|
||||
{ content: "line", type: "insert", lineNumber: 17, isInsert: true },
|
||||
{ content: "line", type: "normal", oldLineNumber: 17, newLineNumber: 18, isNormal: true },
|
||||
{ content: "line", type: "normal", oldLineNumber: 18, newLineNumber: 19, isNormal: true },
|
||||
{ content: "line", type: "normal", oldLineNumber: 19, newLineNumber: 20, isNormal: true }
|
||||
]
|
||||
};
|
||||
const HUNK_2: Hunk = {
|
||||
content: "@@ -21,7 +22,7 @@",
|
||||
oldStart: 21,
|
||||
newStart: 22,
|
||||
oldLines: 7,
|
||||
newLines: 7,
|
||||
changes: [
|
||||
{ content: "line", type: "normal", oldLineNumber: 21, newLineNumber: 22, isNormal: true },
|
||||
{ content: "line", type: "normal", oldLineNumber: 22, newLineNumber: 23, isNormal: true },
|
||||
{ content: "line", type: "normal", oldLineNumber: 23, newLineNumber: 24, isNormal: true },
|
||||
{ content: "line", type: "delete", lineNumber: 24, isDelete: true },
|
||||
{ content: "line", type: "insert", lineNumber: 25, isInsert: true },
|
||||
{ content: "line", type: "normal", oldLineNumber: 25, newLineNumber: 26, isNormal: true },
|
||||
{ content: "line", type: "normal", oldLineNumber: 26, newLineNumber: 27, isNormal: true },
|
||||
{ content: "line", type: "normal", oldLineNumber: 27, newLineNumber: 28, isNormal: true }
|
||||
]
|
||||
};
|
||||
const HUNK_3: Hunk = {
|
||||
content: "@@ -33,6 +34,7 @@",
|
||||
oldStart: 33,
|
||||
newStart: 34,
|
||||
oldLines: 6,
|
||||
newLines: 7,
|
||||
changes: [
|
||||
{ content: "line", type: "normal", oldLineNumber: 33, newLineNumber: 34, isNormal: true },
|
||||
{ content: "line", type: "normal", oldLineNumber: 34, newLineNumber: 35, isNormal: true },
|
||||
{ content: "line", type: "normal", oldLineNumber: 35, newLineNumber: 36, isNormal: true },
|
||||
{ content: "line", type: "insert", lineNumber: 37, isInsert: true },
|
||||
{ content: "line", type: "normal", oldLineNumber: 36, newLineNumber: 38, isNormal: true },
|
||||
{ content: "line", type: "normal", oldLineNumber: 37, newLineNumber: 39, isNormal: true },
|
||||
{ content: "line", type: "normal", oldLineNumber: 38, newLineNumber: 40, isNormal: true }
|
||||
]
|
||||
};
|
||||
const TEST_CONTENT_WITH_HUNKS: File = {
|
||||
hunks: [HUNK_0, HUNK_1, HUNK_2, HUNK_3],
|
||||
newEndingNewLine: true,
|
||||
newPath: "src/main/js/CommitMessage.js",
|
||||
newRevision: "4305a8df175b7bec25acbe542a13fbe2a718a608",
|
||||
oldEndingNewLine: true,
|
||||
oldPath: "src/main/js/CommitMessage.js",
|
||||
oldRevision: "e05c8495bb1dc7505d73af26210c8ff4825c4500",
|
||||
type: "modify",
|
||||
language: "javascript",
|
||||
_links: {
|
||||
lines: {
|
||||
href: "http://localhost:8081/scm/api/v2/content/abc/CommitMessage.js?start={start}&end={end}",
|
||||
templated: true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const TEST_CONTENT_WITH_NEW_BINARY_FILE: File = {
|
||||
oldPath: "/dev/null",
|
||||
newPath: "src/main/fileUploadV2.png",
|
||||
oldEndingNewLine: true,
|
||||
newEndingNewLine: true,
|
||||
oldRevision: "0000000000000000000000000000000000000000",
|
||||
newRevision: "86c370aae0727d628a5438f79a5cdd45752b9d99",
|
||||
type: "add"
|
||||
};
|
||||
|
||||
const TEST_CONTENT_WITH_NEW_TEXT_FILE: File = {
|
||||
oldPath: "/dev/null",
|
||||
newPath: "src/main/markdown/README.md",
|
||||
oldEndingNewLine: true,
|
||||
newEndingNewLine: true,
|
||||
oldRevision: "0000000000000000000000000000000000000000",
|
||||
newRevision: "4e173d365d796b9a9e7562fcd0ef90398ae37046",
|
||||
type: "add",
|
||||
language: "markdown",
|
||||
hunks: [
|
||||
{
|
||||
content: "@@ -0,0 +1,2 @@",
|
||||
newStart: 1,
|
||||
newLines: 2,
|
||||
changes: [
|
||||
{ content: "line 1", type: "insert", lineNumber: 1, isInsert: true },
|
||||
{ content: "line 2", type: "insert", lineNumber: 2, isInsert: true }
|
||||
]
|
||||
}
|
||||
],
|
||||
_links: {
|
||||
lines: {
|
||||
href:
|
||||
"http://localhost:8081/scm/api/v2/repositories/scm-manager/scm-editor-plugin/content/c63898d35520ee47bcc3a8291660979918715762/src/main/markdown/README.md?start={start}&end={end}",
|
||||
templated: true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const TEST_CONTENT_WITH_DELETED_TEXT_FILE: File = {
|
||||
oldPath: "README.md",
|
||||
newPath: "/dev/null",
|
||||
oldEndingNewLine: true,
|
||||
newEndingNewLine: true,
|
||||
oldRevision: "4875ab3b7a1bb117e1948895148557fc5c0b6f75",
|
||||
newRevision: "0000000000000000000000000000000000000000",
|
||||
type: "delete",
|
||||
language: "markdown",
|
||||
hunks: [
|
||||
{
|
||||
content: "@@ -1 +0,0 @@",
|
||||
oldStart: 1,
|
||||
oldLines: 1,
|
||||
changes: [{ content: "# scm-editor-plugin", type: "delete", lineNumber: 1, isDelete: true }]
|
||||
}
|
||||
],
|
||||
_links: { lines: { href: "http://localhost:8081/dev/null?start={start}&end={end}", templated: true } }
|
||||
};
|
||||
|
||||
const TEST_CONTENT_WITH_DELETED_LINES_AT_END: File = {
|
||||
oldPath: "pom.xml",
|
||||
newPath: "pom.xml",
|
||||
oldEndingNewLine: true,
|
||||
newEndingNewLine: true,
|
||||
oldRevision: "b207512c0eab22536c9e5173afbe54cc3a24a22e",
|
||||
newRevision: "5347c3fe0c2c4d4de7f308ae61bd5546460d7e93",
|
||||
type: "modify",
|
||||
language: "xml",
|
||||
hunks: [
|
||||
{
|
||||
content: "@@ -108,15 +108,3 @@",
|
||||
oldStart: 108,
|
||||
newStart: 108,
|
||||
oldLines: 15,
|
||||
newLines: 3,
|
||||
changes: [
|
||||
{ content: "line", type: "normal", oldLineNumber: 108, newLineNumber: 108, isNormal: true },
|
||||
{ content: "line", type: "normal", oldLineNumber: 109, newLineNumber: 109, isNormal: true },
|
||||
{ content: "line", type: "normal", oldLineNumber: 110, newLineNumber: 110, isNormal: true },
|
||||
{ content: "line", type: "delete", lineNumber: 111, isDelete: true },
|
||||
{ content: "line", type: "delete", lineNumber: 112, isDelete: true },
|
||||
{ content: "line", type: "delete", lineNumber: 113, isDelete: true },
|
||||
{ content: "line", type: "delete", lineNumber: 114, isDelete: true },
|
||||
{ content: "line", type: "delete", lineNumber: 115, isDelete: true },
|
||||
{ content: "line", type: "delete", lineNumber: 116, isDelete: true },
|
||||
{ content: "line", type: "delete", lineNumber: 117, isDelete: true },
|
||||
{ content: "line", type: "delete", lineNumber: 118, isDelete: true },
|
||||
{ content: "line", type: "delete", lineNumber: 119, isDelete: true },
|
||||
{ content: "line", type: "delete", lineNumber: 120, isDelete: true },
|
||||
{ content: "line", type: "delete", lineNumber: 121, isDelete: true },
|
||||
{ content: "line", type: "delete", lineNumber: 122, isDelete: true }
|
||||
]
|
||||
}
|
||||
],
|
||||
_links: {
|
||||
lines: {
|
||||
href: "http://localhost:8081/scm/api/v2/content/abc/CommitMessage.js?start={start}&end={end}",
|
||||
templated: true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const TEST_CONTENT_WITH_ALL_LINES_REMOVED_FROM_FILE: File = {
|
||||
oldPath: "pom.xml",
|
||||
newPath: "pom.xml",
|
||||
oldEndingNewLine: true,
|
||||
newEndingNewLine: true,
|
||||
oldRevision: "2cc811c64f71ceda28f1ec0d97e1973395b299ff",
|
||||
newRevision: "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
|
||||
type: "modify",
|
||||
language: "xml",
|
||||
hunks: [
|
||||
{
|
||||
content: "@@ -1,3 +0,0 @@",
|
||||
oldStart: 1,
|
||||
oldLines: 3,
|
||||
changes: [
|
||||
{ content: "line", type: "delete", lineNumber: 1, isDelete: true },
|
||||
{ content: "line", type: "delete", lineNumber: 2, isDelete: true },
|
||||
{ content: "line", type: "delete", lineNumber: 3, isDelete: true }
|
||||
]
|
||||
}
|
||||
],
|
||||
_links: {
|
||||
lines: {
|
||||
href:
|
||||
"http://localhost:8081/scm/api/v2/repositories/scm-manager/scm-editor-plugin/content/b313a7690f028c77df98417c1ed6cba67e5692ec/pom.xml?start={start}&end={end}",
|
||||
templated: true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
describe("with hunks the diff expander", () => {
|
||||
const diffExpander = new DiffExpander(TEST_CONTENT_WITH_HUNKS);
|
||||
|
||||
afterEach(() => {
|
||||
fetchMock.reset();
|
||||
fetchMock.restore();
|
||||
});
|
||||
|
||||
it("should have hunk count from origin", () => {
|
||||
expect(diffExpander.hunkCount()).toBe(4);
|
||||
});
|
||||
|
||||
it("should return correct hunk", () => {
|
||||
expect(diffExpander.getHunk(1).hunk).toBe(HUNK_1);
|
||||
});
|
||||
|
||||
it("should return max expand head range for first hunk", () => {
|
||||
expect(diffExpander.getHunk(0).maxExpandHeadRange).toBe(0);
|
||||
});
|
||||
|
||||
it("should return max expand head range for hunks in the middle", () => {
|
||||
expect(diffExpander.getHunk(1).maxExpandHeadRange).toBe(5);
|
||||
});
|
||||
|
||||
it("should return max expand bottom range for hunks in the middle", () => {
|
||||
expect(diffExpander.getHunk(1).maxExpandBottomRange).toBe(1);
|
||||
});
|
||||
|
||||
it("should return a really bix number for the expand bottom range of the last hunk", () => {
|
||||
expect(diffExpander.getHunk(3).maxExpandBottomRange).toBe(-1);
|
||||
});
|
||||
it("should create new hunk with new line from api client at the bottom", async () => {
|
||||
expect(diffExpander.getHunk(1).hunk.changes.length).toBe(7);
|
||||
const oldHunkCount = diffExpander.hunkCount();
|
||||
const expandedHunk = diffExpander.getHunk(1).hunk;
|
||||
const subsequentHunk = diffExpander.getHunk(2).hunk;
|
||||
fetchMock.get("http://localhost:8081/scm/api/v2/content/abc/CommitMessage.js?start=20&end=21", "new line 1");
|
||||
let newFile: File;
|
||||
await diffExpander
|
||||
.getHunk(1)
|
||||
.expandBottom(1)
|
||||
.then(file => (newFile = file));
|
||||
expect(fetchMock.done()).toBe(true);
|
||||
expect(newFile!.hunks!.length).toBe(oldHunkCount + 1);
|
||||
expect(newFile!.hunks![1]).toBe(expandedHunk);
|
||||
|
||||
const newHunk = newFile!.hunks![2];
|
||||
expect(newHunk.changes.length).toBe(1);
|
||||
expect(newHunk.changes[0].content).toBe("new line 1");
|
||||
expect(newHunk.expansion).toBe(true);
|
||||
|
||||
expect(newFile!.hunks![3]).toBe(subsequentHunk);
|
||||
});
|
||||
it("should create new hunk with new line from api client at the top", async () => {
|
||||
expect(diffExpander.getHunk(1).hunk.changes.length).toBe(7);
|
||||
const oldHunkCount = diffExpander.hunkCount();
|
||||
const expandedHunk = diffExpander.getHunk(1).hunk;
|
||||
const preceedingHunk = diffExpander.getHunk(0).hunk;
|
||||
fetchMock.get(
|
||||
"http://localhost:8081/scm/api/v2/content/abc/CommitMessage.js?start=8&end=13",
|
||||
"new line 9\nnew line 10\nnew line 11\nnew line 12\nnew line 13"
|
||||
);
|
||||
let newFile: File;
|
||||
await diffExpander
|
||||
.getHunk(1)
|
||||
.expandHead(5)
|
||||
.then(file => (newFile = file));
|
||||
expect(fetchMock.done()).toBe(true);
|
||||
expect(newFile!.hunks!.length).toBe(oldHunkCount + 1);
|
||||
expect(newFile!.hunks![0]).toBe(preceedingHunk);
|
||||
expect(newFile!.hunks![2]).toBe(expandedHunk);
|
||||
|
||||
const newHunk = newFile!.hunks![1];
|
||||
expect(newHunk.changes.length).toBe(5);
|
||||
expect(newHunk.changes[0].content).toBe("new line 9");
|
||||
expect(newHunk.changes[0].oldLineNumber).toBe(9);
|
||||
expect(newHunk.changes[0].newLineNumber).toBe(9);
|
||||
expect(newHunk.changes[1].content).toBe("new line 10");
|
||||
expect(newHunk.changes[1].oldLineNumber).toBe(10);
|
||||
expect(newHunk.changes[1].newLineNumber).toBe(10);
|
||||
expect(newHunk.changes[4].content).toBe("new line 13");
|
||||
expect(newHunk.changes[4].oldLineNumber).toBe(13);
|
||||
expect(newHunk.changes[4].newLineNumber).toBe(13);
|
||||
expect(newHunk.expansion).toBe(true);
|
||||
});
|
||||
it("should set fully expanded to true if expanded completely", async () => {
|
||||
const oldHunkCount = diffExpander.hunkCount();
|
||||
fetchMock.get(
|
||||
"http://localhost:8081/scm/api/v2/content/abc/CommitMessage.js?start=40&end=50",
|
||||
"new line 40\nnew line 41\nnew line 42"
|
||||
);
|
||||
let newFile: File;
|
||||
await diffExpander
|
||||
.getHunk(3)
|
||||
.expandBottom(10)
|
||||
.then(file => (newFile = file));
|
||||
expect(newFile!.hunks!.length).toBe(oldHunkCount + 1);
|
||||
expect(newFile!.hunks![4].fullyExpanded).toBe(true);
|
||||
});
|
||||
it("should set end to -1 if requested to expand to the end", async () => {
|
||||
fetchMock.get(
|
||||
"http://localhost:8081/scm/api/v2/content/abc/CommitMessage.js?start=40&end=-1",
|
||||
"new line 40\nnew line 41\nnew line 42"
|
||||
);
|
||||
let newFile: File;
|
||||
await diffExpander
|
||||
.getHunk(3)
|
||||
.expandBottom(-1)
|
||||
.then(file => (newFile = file));
|
||||
await fetchMock.flush(true);
|
||||
expect(newFile!.hunks![4].fullyExpanded).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("for a new file with text input the diff expander", () => {
|
||||
const diffExpander = new DiffExpander(TEST_CONTENT_WITH_NEW_TEXT_FILE);
|
||||
it("should create answer for single hunk", () => {
|
||||
expect(diffExpander.hunkCount()).toBe(1);
|
||||
});
|
||||
it("should neither give expandable lines for top nor bottom", () => {
|
||||
const hunk = diffExpander.getHunk(0);
|
||||
expect(hunk.maxExpandHeadRange).toBe(0);
|
||||
expect(hunk.maxExpandBottomRange).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("for a deleted file with text input the diff expander", () => {
|
||||
const diffExpander = new DiffExpander(TEST_CONTENT_WITH_DELETED_TEXT_FILE);
|
||||
it("should create answer for single hunk", () => {
|
||||
expect(diffExpander.hunkCount()).toBe(1);
|
||||
});
|
||||
it("should neither give expandable lines for top nor bottom", () => {
|
||||
const hunk = diffExpander.getHunk(0);
|
||||
expect(hunk.maxExpandHeadRange).toBe(0);
|
||||
expect(hunk.maxExpandBottomRange).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("for a new file with binary input the diff expander", () => {
|
||||
const diffExpander = new DiffExpander(TEST_CONTENT_WITH_NEW_BINARY_FILE);
|
||||
it("should create answer for no hunk", () => {
|
||||
expect(diffExpander.hunkCount()).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with deleted lines at the end", () => {
|
||||
const diffExpander = new DiffExpander(TEST_CONTENT_WITH_DELETED_LINES_AT_END);
|
||||
it("should not be expandable", () => {
|
||||
expect(diffExpander.getHunk(0)!.maxExpandBottomRange).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with all lines removed from file", () => {
|
||||
const diffExpander = new DiffExpander(TEST_CONTENT_WITH_ALL_LINES_REMOVED_FROM_FILE);
|
||||
it("should not be expandable", () => {
|
||||
expect(diffExpander.getHunk(0)!.maxExpandBottomRange).toBe(0);
|
||||
});
|
||||
});
|
||||
198
scm-ui/ui-components/src/repos/DiffExpander.ts
Normal file
198
scm-ui/ui-components/src/repos/DiffExpander.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import { apiClient } from "@scm-manager/ui-components";
|
||||
import { Change, File, Hunk } from "./DiffTypes";
|
||||
import { Link } from "@scm-manager/ui-types";
|
||||
|
||||
class DiffExpander {
|
||||
file: File;
|
||||
|
||||
constructor(file: File) {
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
hunkCount = () => {
|
||||
if (this.file.hunks) {
|
||||
return this.file.hunks.length;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
minLineNumber: (n: number) => number = (n: number) => {
|
||||
return this.file.hunks![n]!.newStart!;
|
||||
};
|
||||
|
||||
maxLineNumber: (n: number) => number = (n: number) => {
|
||||
return this.file.hunks![n]!.newStart! + this.file.hunks![n]!.newLines! - 1;
|
||||
};
|
||||
|
||||
computeMaxExpandHeadRange = (n: number) => {
|
||||
if (this.file.type === "delete") {
|
||||
return 0;
|
||||
} else if (n === 0) {
|
||||
return this.minLineNumber(n) - 1;
|
||||
}
|
||||
return this.minLineNumber(n) - this.maxLineNumber(n - 1) - 1;
|
||||
};
|
||||
|
||||
computeMaxExpandBottomRange = (n: number) => {
|
||||
if (this.file.type === "add" || this.file.type === "delete") {
|
||||
return 0;
|
||||
}
|
||||
const changes = this.file.hunks![n].changes;
|
||||
if (changes[changes.length - 1].type === "normal") {
|
||||
if (n === this.file!.hunks!.length - 1) {
|
||||
return this.file!.hunks![this.file!.hunks!.length - 1].fullyExpanded ? 0 : -1;
|
||||
}
|
||||
return this.minLineNumber(n + 1) - this.maxLineNumber(n) - 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
expandHead: (n: number, count: number) => Promise<File> = (n, count) => {
|
||||
const start = this.minLineNumber(n) - Math.min(count, this.computeMaxExpandHeadRange(n)) - 1;
|
||||
const end = this.minLineNumber(n) - 1;
|
||||
return this.loadLines(start, end).then(lines => {
|
||||
const hunk = this.file.hunks![n];
|
||||
|
||||
const newHunk = this.createNewHunk(
|
||||
hunk.oldStart! - lines.length,
|
||||
hunk.newStart! - lines.length,
|
||||
lines,
|
||||
lines.length
|
||||
);
|
||||
|
||||
return this.addHunkToFile(newHunk, n);
|
||||
});
|
||||
};
|
||||
|
||||
expandBottom: (n: number, count: number) => Promise<File> = (n, count) => {
|
||||
const maxExpandBottomRange = this.computeMaxExpandBottomRange(n);
|
||||
const start = this.maxLineNumber(n);
|
||||
const end =
|
||||
count > 0
|
||||
? start + Math.min(count, maxExpandBottomRange > 0 ? maxExpandBottomRange : Number.MAX_SAFE_INTEGER)
|
||||
: -1;
|
||||
return this.loadLines(start, end).then(lines => {
|
||||
const hunk = this.file.hunks![n];
|
||||
|
||||
const newHunk: Hunk = this.createNewHunk(
|
||||
this.getMaxOldLineNumber(hunk.changes) + 1,
|
||||
this.getMaxNewLineNumber(hunk.changes) + 1,
|
||||
lines,
|
||||
count
|
||||
);
|
||||
|
||||
return this.addHunkToFile(newHunk, n + 1);
|
||||
});
|
||||
};
|
||||
|
||||
loadLines = (start: number, end: number) => {
|
||||
const lineRequestUrl = (this.file._links!.lines as Link).href
|
||||
.replace("{start}", start.toString())
|
||||
.replace("{end}", end.toString());
|
||||
return apiClient
|
||||
.get(lineRequestUrl)
|
||||
.then(response => response.text())
|
||||
.then(text => text.split("\n"))
|
||||
.then(lines => (lines[lines.length - 1] === "" ? lines.slice(0, lines.length - 1) : lines));
|
||||
};
|
||||
|
||||
addHunkToFile = (newHunk: Hunk, position: number) => {
|
||||
const newHunks: Hunk[] = [];
|
||||
this.file.hunks!.forEach((oldHunk: Hunk, i: number) => {
|
||||
if (i === position) {
|
||||
newHunks.push(newHunk);
|
||||
}
|
||||
newHunks.push(oldHunk);
|
||||
});
|
||||
if (position === newHunks.length) {
|
||||
newHunks.push(newHunk);
|
||||
}
|
||||
return { ...this.file, hunks: newHunks };
|
||||
};
|
||||
|
||||
createNewHunk = (oldFirstLineNumber: number, newFirstLineNumber: number, lines: string[], requestedLines: number) => {
|
||||
const newChanges: Change[] = [];
|
||||
|
||||
let oldLineNumber: number = oldFirstLineNumber;
|
||||
let newLineNumber: number = newFirstLineNumber;
|
||||
|
||||
lines.forEach(line => {
|
||||
newChanges.push({
|
||||
content: line,
|
||||
type: "normal",
|
||||
oldLineNumber,
|
||||
newLineNumber,
|
||||
isNormal: true
|
||||
});
|
||||
oldLineNumber += 1;
|
||||
newLineNumber += 1;
|
||||
});
|
||||
|
||||
return {
|
||||
changes: newChanges,
|
||||
content: "",
|
||||
oldStart: oldFirstLineNumber,
|
||||
newStart: newFirstLineNumber,
|
||||
oldLines: lines.length,
|
||||
newLines: lines.length,
|
||||
expansion: true,
|
||||
fullyExpanded: requestedLines < 0 || lines.length < requestedLines
|
||||
};
|
||||
};
|
||||
|
||||
getMaxOldLineNumber = (newChanges: Change[]) => {
|
||||
const lastChange = newChanges[newChanges.length - 1];
|
||||
return lastChange.oldLineNumber || lastChange.lineNumber!;
|
||||
};
|
||||
|
||||
getMaxNewLineNumber = (newChanges: Change[]) => {
|
||||
const lastChange = newChanges[newChanges.length - 1];
|
||||
return lastChange.newLineNumber || lastChange.lineNumber!;
|
||||
};
|
||||
|
||||
getHunk: (n: number) => ExpandableHunk = n => {
|
||||
return {
|
||||
maxExpandHeadRange: this.computeMaxExpandHeadRange(n),
|
||||
maxExpandBottomRange: this.computeMaxExpandBottomRange(n),
|
||||
expandHead: (count: number) => this.expandHead(n, count),
|
||||
expandBottom: (count: number) => this.expandBottom(n, count),
|
||||
hunk: this.file?.hunks![n]
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export type ExpandableHunk = {
|
||||
hunk: Hunk;
|
||||
maxExpandHeadRange: number;
|
||||
maxExpandBottomRange: number;
|
||||
expandHead: (count: number) => Promise<File>;
|
||||
expandBottom: (count: number) => Promise<File>;
|
||||
};
|
||||
|
||||
export default DiffExpander;
|
||||
@@ -34,6 +34,11 @@ import { Change, ChangeEvent, DiffObjectProps, File, Hunk as HunkType } from "./
|
||||
import TokenizedDiffView from "./TokenizedDiffView";
|
||||
import DiffButton from "./DiffButton";
|
||||
import { MenuContext } from "@scm-manager/ui-components";
|
||||
import DiffExpander, { ExpandableHunk } from "./DiffExpander";
|
||||
import HunkExpandLink from "./HunkExpandLink";
|
||||
import { Modal } from "../modals";
|
||||
import ErrorNotification from "../ErrorNotification";
|
||||
import HunkExpandDivider from "./HunkExpandDivider";
|
||||
|
||||
const EMPTY_ANNOTATION_FACTORY = {};
|
||||
|
||||
@@ -47,7 +52,10 @@ type Collapsible = {
|
||||
};
|
||||
|
||||
type State = Collapsible & {
|
||||
file: File;
|
||||
sideBySide?: boolean;
|
||||
diffExpander: DiffExpander;
|
||||
expansionError?: any;
|
||||
};
|
||||
|
||||
const DiffFilePanel = styled.div`
|
||||
@@ -92,7 +100,9 @@ class DiffFile extends React.Component<Props, State> {
|
||||
super(props);
|
||||
this.state = {
|
||||
collapsed: this.defaultCollapse(),
|
||||
sideBySide: props.sideBySide
|
||||
sideBySide: props.sideBySide,
|
||||
diffExpander: new DiffExpander(props.file),
|
||||
file: props.file
|
||||
};
|
||||
}
|
||||
|
||||
@@ -116,7 +126,7 @@ class DiffFile extends React.Component<Props, State> {
|
||||
};
|
||||
|
||||
toggleCollapse = () => {
|
||||
const { file } = this.props;
|
||||
const { file } = this.state;
|
||||
if (this.hasContent(file)) {
|
||||
this.setState(state => ({
|
||||
collapsed: !state.collapsed
|
||||
@@ -139,16 +149,122 @@ class DiffFile extends React.Component<Props, State> {
|
||||
});
|
||||
};
|
||||
|
||||
createHunkHeader = (hunk: HunkType, i: number) => {
|
||||
if (i > 0) {
|
||||
return <HunkDivider />;
|
||||
createHunkHeader = (expandableHunk: ExpandableHunk) => {
|
||||
if (expandableHunk.maxExpandHeadRange > 0) {
|
||||
if (expandableHunk.maxExpandHeadRange <= 10) {
|
||||
return (
|
||||
<HunkExpandDivider>
|
||||
<HunkExpandLink
|
||||
icon={"fa-angle-double-up"}
|
||||
onClick={this.expandHead(expandableHunk, expandableHunk.maxExpandHeadRange)}
|
||||
text={this.props.t("diff.expandComplete", { count: expandableHunk.maxExpandHeadRange })}
|
||||
/>
|
||||
</HunkExpandDivider>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<HunkExpandDivider>
|
||||
<HunkExpandLink
|
||||
icon={"fa-angle-up"}
|
||||
onClick={this.expandHead(expandableHunk, 10)}
|
||||
text={this.props.t("diff.expandByLines", { count: 10 })}
|
||||
/>{" "}
|
||||
<HunkExpandLink
|
||||
icon={"fa-angle-double-up"}
|
||||
onClick={this.expandHead(expandableHunk, expandableHunk.maxExpandHeadRange)}
|
||||
text={this.props.t("diff.expandComplete", { count: expandableHunk.maxExpandHeadRange })}
|
||||
/>
|
||||
</HunkExpandDivider>
|
||||
);
|
||||
}
|
||||
}
|
||||
// hunk header must be defined
|
||||
return <span />;
|
||||
};
|
||||
|
||||
createHunkFooter = (expandableHunk: ExpandableHunk) => {
|
||||
if (expandableHunk.maxExpandBottomRange > 0) {
|
||||
if (expandableHunk.maxExpandBottomRange <= 10) {
|
||||
return (
|
||||
<HunkExpandDivider>
|
||||
<HunkExpandLink
|
||||
icon={"fa-angle-double-down"}
|
||||
onClick={this.expandBottom(expandableHunk, expandableHunk.maxExpandBottomRange)}
|
||||
text={this.props.t("diff.expandComplete", { count: expandableHunk.maxExpandBottomRange })}
|
||||
/>
|
||||
</HunkExpandDivider>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<HunkExpandDivider>
|
||||
<HunkExpandLink
|
||||
icon={"fa-angle-down"}
|
||||
onClick={this.expandBottom(expandableHunk, 10)}
|
||||
text={this.props.t("diff.expandByLines", { count: 10 })}
|
||||
/>{" "}
|
||||
<HunkExpandLink
|
||||
icon={"fa-angle-double-down"}
|
||||
onClick={this.expandBottom(expandableHunk, expandableHunk.maxExpandBottomRange)}
|
||||
text={this.props.t("diff.expandComplete", { count: expandableHunk.maxExpandBottomRange })}
|
||||
/>
|
||||
</HunkExpandDivider>
|
||||
);
|
||||
}
|
||||
}
|
||||
// hunk footer must be defined
|
||||
return <span />;
|
||||
};
|
||||
|
||||
createLastHunkFooter = (expandableHunk: ExpandableHunk) => {
|
||||
if (expandableHunk.maxExpandBottomRange !== 0) {
|
||||
return (
|
||||
<HunkExpandDivider>
|
||||
<HunkExpandLink
|
||||
icon={"fa-angle-down"}
|
||||
onClick={this.expandBottom(expandableHunk, 10)}
|
||||
text={this.props.t("diff.expandLastBottomByLines", { count: 10 })}
|
||||
/>{" "}
|
||||
<HunkExpandLink
|
||||
icon={"fa-angle-double-down"}
|
||||
onClick={this.expandBottom(expandableHunk, expandableHunk.maxExpandBottomRange)}
|
||||
text={this.props.t("diff.expandLastBottomComplete")}
|
||||
/>
|
||||
</HunkExpandDivider>
|
||||
);
|
||||
}
|
||||
// hunk header must be defined
|
||||
return <span />;
|
||||
};
|
||||
|
||||
expandHead = (expandableHunk: ExpandableHunk, count: number) => {
|
||||
return () => {
|
||||
return expandableHunk
|
||||
.expandHead(count)
|
||||
.then(this.diffExpanded)
|
||||
.catch(this.diffExpansionFailed);
|
||||
};
|
||||
};
|
||||
|
||||
expandBottom = (expandableHunk: ExpandableHunk, count: number) => {
|
||||
return () => {
|
||||
return expandableHunk
|
||||
.expandBottom(count)
|
||||
.then(this.diffExpanded)
|
||||
.catch(this.diffExpansionFailed);
|
||||
};
|
||||
};
|
||||
|
||||
diffExpanded = (newFile: File) => {
|
||||
this.setState({ file: newFile, diffExpander: new DiffExpander(newFile) });
|
||||
};
|
||||
|
||||
diffExpansionFailed = (err: any) => {
|
||||
this.setState({ expansionError: err });
|
||||
};
|
||||
|
||||
collectHunkAnnotations = (hunk: HunkType) => {
|
||||
const { annotationFactory, file } = this.props;
|
||||
const { annotationFactory } = this.props;
|
||||
const { file } = this.state;
|
||||
if (annotationFactory) {
|
||||
return annotationFactory({
|
||||
hunk,
|
||||
@@ -160,7 +276,8 @@ class DiffFile extends React.Component<Props, State> {
|
||||
};
|
||||
|
||||
handleClickEvent = (change: Change, hunk: HunkType) => {
|
||||
const { file, onClick } = this.props;
|
||||
const { onClick } = this.props;
|
||||
const { file } = this.state;
|
||||
const context = {
|
||||
changeId: getChangeKey(change),
|
||||
change,
|
||||
@@ -183,19 +300,35 @@ class DiffFile extends React.Component<Props, State> {
|
||||
}
|
||||
};
|
||||
|
||||
renderHunk = (hunk: HunkType, i: number) => {
|
||||
renderHunk = (file: File, expandableHunk: ExpandableHunk, i: number) => {
|
||||
const hunk = expandableHunk.hunk;
|
||||
if (this.props.markConflicts && hunk.changes) {
|
||||
this.markConflicts(hunk);
|
||||
}
|
||||
return [
|
||||
<Decoration key={"decoration-" + hunk.content}>{this.createHunkHeader(hunk, i)}</Decoration>,
|
||||
const items = [];
|
||||
if (file._links?.lines) {
|
||||
items.push(this.createHunkHeader(expandableHunk));
|
||||
} else if (i > 0) {
|
||||
items.push(<Decoration><HunkDivider /></Decoration>);
|
||||
}
|
||||
|
||||
items.push(
|
||||
<Hunk
|
||||
key={"hunk-" + hunk.content}
|
||||
hunk={hunk}
|
||||
hunk={expandableHunk.hunk}
|
||||
widgets={this.collectHunkAnnotations(hunk)}
|
||||
gutterEvents={this.createGutterEvents(hunk)}
|
||||
className={this.props.hunkClass ? this.props.hunkClass(hunk) : null}
|
||||
/>
|
||||
];
|
||||
);
|
||||
if (file._links?.lines) {
|
||||
if (i === file.hunks!.length - 1) {
|
||||
items.push(this.createLastHunkFooter(expandableHunk));
|
||||
} else {
|
||||
items.push(this.createHunkFooter(expandableHunk));
|
||||
}
|
||||
}
|
||||
return items;
|
||||
};
|
||||
|
||||
markConflicts = (hunk: HunkType) => {
|
||||
@@ -251,19 +384,11 @@ class DiffFile extends React.Component<Props, State> {
|
||||
return <ChangeTypeTag className={classNames("is-rounded", "has-text-weight-normal")} color={color} label={value} />;
|
||||
};
|
||||
|
||||
concat = (array: object[][]) => {
|
||||
if (array.length > 0) {
|
||||
return array.reduce((a, b) => a.concat(b));
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
hasContent = (file: File) => file && !file.isBinary && file.hunks && file.hunks.length > 0;
|
||||
|
||||
render() {
|
||||
const { file, fileControlFactory, fileAnnotationFactory, t } = this.props;
|
||||
const { collapsed, sideBySide } = this.state;
|
||||
const { fileControlFactory, fileAnnotationFactory, t } = this.props;
|
||||
const { file, collapsed, sideBySide, diffExpander, expansionError } = this.state;
|
||||
const viewType = sideBySide ? "split" : "unified";
|
||||
|
||||
let body = null;
|
||||
@@ -275,7 +400,11 @@ class DiffFile extends React.Component<Props, State> {
|
||||
<div className="panel-block is-paddingless">
|
||||
{fileAnnotations}
|
||||
<TokenizedDiffView className={viewType} viewType={viewType} file={file}>
|
||||
{(hunks: HunkType[]) => this.concat(hunks.map(this.renderHunk))}
|
||||
{(hunks: HunkType[]) =>
|
||||
hunks?.map((hunk, n) => {
|
||||
return this.renderHunk(file, diffExpander.getHunk(n), n);
|
||||
})
|
||||
}
|
||||
</TokenizedDiffView>
|
||||
</div>
|
||||
);
|
||||
@@ -306,8 +435,21 @@ class DiffFile extends React.Component<Props, State> {
|
||||
</ButtonWrapper>
|
||||
) : null;
|
||||
|
||||
let errorModal;
|
||||
if (expansionError) {
|
||||
errorModal = (
|
||||
<Modal
|
||||
title={t("diff.expansionFailed")}
|
||||
closeFunction={() => this.setState({ expansionError: undefined })}
|
||||
body={<ErrorNotification error={expansionError} />}
|
||||
active={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DiffFilePanel className={classNames("panel", "is-size-6")} collapsed={(file && file.isBinary) || collapsed}>
|
||||
{errorModal}
|
||||
<div className="panel-heading">
|
||||
<FlexWrapLevel className="level">
|
||||
<FullWidthTitleHeader
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
import { ReactNode } from "react";
|
||||
import { DefaultCollapsed } from "./defaultCollapsed";
|
||||
import { Links } from "@scm-manager/ui-types";
|
||||
|
||||
// We place the types here and not in @scm-manager/ui-types,
|
||||
// because they represent not a real scm-manager related type.
|
||||
@@ -33,7 +34,7 @@ import { DefaultCollapsed } from "./defaultCollapsed";
|
||||
export type FileChangeType = "add" | "modify" | "delete" | "copy" | "rename";
|
||||
|
||||
export type File = {
|
||||
hunks: Hunk[];
|
||||
hunks?: Hunk[];
|
||||
newEndingNewLine: boolean;
|
||||
newMode?: string;
|
||||
newPath: string;
|
||||
@@ -46,11 +47,18 @@ export type File = {
|
||||
language?: string;
|
||||
// TODO does this property exists?
|
||||
isBinary?: boolean;
|
||||
_links?: Links;
|
||||
};
|
||||
|
||||
export type Hunk = {
|
||||
changes: Change[];
|
||||
content: string;
|
||||
oldStart?: number;
|
||||
newStart?: number;
|
||||
oldLines?: number;
|
||||
newLines?: number;
|
||||
fullyExpanded?: boolean;
|
||||
expansion?: boolean;
|
||||
};
|
||||
|
||||
export type ChangeType = "insert" | "delete" | "normal" | "conflict";
|
||||
@@ -103,4 +111,5 @@ export type DiffObjectProps = {
|
||||
annotationFactory?: AnnotationFactory;
|
||||
markConflicts?: boolean;
|
||||
defaultCollapse?: DefaultCollapsed;
|
||||
hunkClass?: (hunk: Hunk) => string;
|
||||
};
|
||||
|
||||
43
scm-ui/ui-components/src/repos/HunkExpandDivider.tsx
Normal file
43
scm-ui/ui-components/src/repos/HunkExpandDivider.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React, { FC } from "react";
|
||||
// @ts-ignore
|
||||
import { Decoration } from "react-diff-view";
|
||||
import styled from "styled-components";
|
||||
|
||||
const HunkDivider = styled.div`
|
||||
background: #98d8f3;
|
||||
font-size: 0.7rem;
|
||||
padding-left: 1.78em;
|
||||
`;
|
||||
|
||||
const HunkExpandDivider: FC = ({ children }) => {
|
||||
return (
|
||||
<Decoration>
|
||||
<HunkDivider>{children}</HunkDivider>
|
||||
</Decoration>
|
||||
);
|
||||
};
|
||||
|
||||
export default HunkExpandDivider;
|
||||
58
scm-ui/ui-components/src/repos/HunkExpandLink.tsx
Normal file
58
scm-ui/ui-components/src/repos/HunkExpandLink.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React, { FC, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import classNames from "classnames";
|
||||
import styled from "styled-components";
|
||||
|
||||
type Props = {
|
||||
icon: string;
|
||||
text: string;
|
||||
onClick: () => Promise<any>;
|
||||
};
|
||||
|
||||
const ExpandLink = styled.span`
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
const HunkExpandLink: FC<Props> = ({ icon, text, onClick }) => {
|
||||
const [t] = useTranslation("repos");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const onClickWithLoadingMarker = () => {
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
onClick().then(() => setLoading(false));
|
||||
};
|
||||
|
||||
return (
|
||||
<ExpandLink onClick={onClickWithLoadingMarker}>
|
||||
<i className={classNames("fa", icon)} /> {loading ? t("diff.expanding") : text}
|
||||
</ExpandLink>
|
||||
);
|
||||
};
|
||||
|
||||
export default HunkExpandLink;
|
||||
Reference in New Issue
Block a user