merge repository heads

This commit is contained in:
Sebastian Sdorra
2019-12-18 14:55:38 +01:00
35 changed files with 1012 additions and 465 deletions

View File

@@ -101,7 +101,9 @@
"length": "Größe",
"lastModified": "Zuletzt bearbeitet",
"description": "Beschreibung",
"branch": "Branch"
"branch": "Branch",
"notYetComputed": "Noch nicht berechnet; Der Wert wird in Kürze aktualisiert",
"computationAborted": "Die Berechnung dauert zu lange und wurde abgebrochen"
},
"content": {
"historyButton": "History",

View File

@@ -101,7 +101,9 @@
"length": "Length",
"lastModified": "Last modified",
"description": "Description",
"branch": "Branch"
"branch": "Branch",
"notYetComputed": "Not yet computed, will be updated in a short while",
"computationAborted": "The computation took too long and was aborted"
},
"content": {
"historyButton": "History",

View File

@@ -7,7 +7,7 @@ import styled from "styled-components";
import { binder } from "@scm-manager/ui-extensions";
import { Repository, File } from "@scm-manager/ui-types";
import { ErrorNotification, Loading, Notification } from "@scm-manager/ui-components";
import { getFetchSourcesFailure, isFetchSourcesPending, getSources } from "../modules/sources";
import { getFetchSourcesFailure, isFetchSourcesPending, getSources, fetchSources } from "../modules/sources";
import FileTreeLeaf from "./FileTreeLeaf";
type Props = WithTranslation & {
@@ -19,10 +19,16 @@ type Props = WithTranslation & {
path: string;
baseUrl: string;
updateSources: () => void;
// context props
match: any;
};
type State = {
stoppableUpdateHandler?: number;
};
const FixedWidthTh = styled.th`
width: 16px;
`;
@@ -39,7 +45,28 @@ export function findParent(path: string) {
return "";
}
class FileTree extends React.Component<Props> {
class FileTree extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {};
}
componentDidUpdate(prevProps: Readonly<Props>, prevState: Readonly<State>): void {
if (prevState.stoppableUpdateHandler === this.state.stoppableUpdateHandler) {
const { tree, updateSources } = this.props;
if (tree?._embedded?.children && tree._embedded.children.find(c => c.partialResult)) {
const stoppableUpdateHandler = setTimeout(updateSources, 3000);
this.setState({ stoppableUpdateHandler: stoppableUpdateHandler });
}
}
}
componentWillUnmount(): void {
if (this.state.stoppableUpdateHandler) {
clearTimeout(this.state.stoppableUpdateHandler);
}
}
render() {
const { error, loading, tree } = this.props;
@@ -106,7 +133,7 @@ class FileTree extends React.Component<Props> {
<FixedWidthTh />
<th>{t("sources.file-tree.name")}</th>
<th className="is-hidden-mobile">{t("sources.file-tree.length")}</th>
<th className="is-hidden-mobile">{t("sources.file-tree.lastModified")}</th>
<th className="is-hidden-mobile">{t("sources.file-tree.commitDate")}</th>
<th className="is-hidden-touch">{t("sources.file-tree.description")}</th>
{binder.hasExtension("repos.sources.tree.row.right") && <th className="is-hidden-mobile" />}
</tr>
@@ -123,6 +150,14 @@ class FileTree extends React.Component<Props> {
}
}
const mapDispatchToProps = (dispatch: any, ownProps: Props) => {
const { repository, revision, path } = ownProps;
const updateSources = () => dispatch(fetchSources(repository, revision, path, false));
return { updateSources };
};
const mapStateToProps = (state: any, ownProps: Props) => {
const { repository, revision, path } = ownProps;
@@ -141,5 +176,8 @@ const mapStateToProps = (state: any, ownProps: Props) => {
export default compose(
withRouter,
connect(mapStateToProps)
connect(
mapStateToProps,
mapDispatchToProps
)
)(withTranslation("repos")(FileTree));

View File

@@ -4,10 +4,12 @@ import classNames from "classnames";
import styled from "styled-components";
import { binder, ExtensionPoint } from "@scm-manager/ui-extensions";
import { File } from "@scm-manager/ui-types";
import { DateFromNow, FileSize } from "@scm-manager/ui-components";
import { DateFromNow, FileSize, Tooltip } from "@scm-manager/ui-components";
import FileIcon from "./FileIcon";
import { Icon } from "@scm-manager/ui-components/src";
import { WithTranslation, withTranslation } from "react-i18next";
type Props = {
type Props = WithTranslation & {
file: File;
baseUrl: string;
};
@@ -35,7 +37,7 @@ export function createLink(base: string, file: File) {
return link;
}
export default class FileTreeLeaf extends React.Component<Props> {
class FileTreeLeaf extends React.Component<Props> {
createLink = (file: File) => {
return createLink(this.props.baseUrl, file);
};
@@ -62,20 +64,42 @@ export default class FileTreeLeaf extends React.Component<Props> {
return <Link to={this.createLink(file)}>{file.name}</Link>;
};
contentIfPresent = (file: File, attribute: string, content: (file: File) => any) => {
const { t } = this.props;
if (file.hasOwnProperty(attribute)) {
return content(file);
} else if (file.computationAborted) {
return (
<Tooltip location="top" message={t("sources.file-tree.computationAborted")}>
<Icon name={"question-circle"} />
</Tooltip>
);
} else if (file.partialResult) {
return (
<Tooltip location="top" message={t("sources.file-tree.notYetComputed")}>
<Icon name={"hourglass"} />
</Tooltip>
);
} else {
return content(file);
}
};
render() {
const { file } = this.props;
const fileSize = file.directory ? "" : <FileSize bytes={file.length} />;
const renderFileSize = (file: File) => <FileSize bytes={file.length} />;
const renderCommitDate = (file: File) => <DateFromNow date={file.commitDate} />;
return (
<tr>
<td>{this.createFileIcon(file)}</td>
<MinWidthTd className="is-word-break">{this.createFileName(file)}</MinWidthTd>
<NoWrapTd className="is-hidden-mobile">{fileSize}</NoWrapTd>
<td className="is-hidden-mobile">
<DateFromNow date={file.lastModified} />
</td>
<MinWidthTd className={classNames("is-word-break", "is-hidden-touch")}>{file.description}</MinWidthTd>
<NoWrapTd className="is-hidden-mobile">{file.directory ? "" : this.contentIfPresent(file, "length", renderFileSize)}</NoWrapTd>
<td className="is-hidden-mobile">{this.contentIfPresent(file, "commitDate", renderCommitDate)}</td>
<MinWidthTd className={classNames("is-word-break", "is-hidden-touch")}>
{this.contentIfPresent(file, "description", file => file.description)}
</MinWidthTd>
{binder.hasExtension("repos.sources.tree.row.right") && (
<td className="is-hidden-mobile">
{!file.directory && (
@@ -93,3 +117,5 @@ export default class FileTreeLeaf extends React.Component<Props> {
);
}
}
export default withTranslation("repos")(FileTreeLeaf);

View File

@@ -115,7 +115,7 @@ class Content extends React.Component<Props, State> {
showMoreInformation() {
const collapsed = this.state.collapsed;
const { file, revision, t, repository } = this.props;
const date = <DateFromNow date={file.lastModified} />;
const date = <DateFromNow date={file.commitDate} />;
const description = file.description ? (
<p>
{file.description.split("\n").map((item, key) => {

View File

@@ -49,10 +49,8 @@ const collection = {
name: "src",
path: "src",
directory: true,
description: "",
length: 176,
revision: "76aae4bb4ceacf0e88938eb5b6832738b7d537b4",
lastModified: "",
subRepository: undefined,
_links: {
self: {
@@ -71,7 +69,7 @@ const collection = {
description: "bump version",
length: 780,
revision: "76aae4bb4ceacf0e88938eb5b6832738b7d537b4",
lastModified: "2017-07-31T11:17:19Z",
commitDate: "2017-07-31T11:17:19Z",
subRepository: undefined,
_links: {
self: {
@@ -127,7 +125,7 @@ describe("sources fetch", () => {
{
type: FETCH_SOURCES_SUCCESS,
itemId: "scm/core/_/",
payload: collection
payload: { updatePending: false, sources: collection }
}
];
@@ -148,7 +146,7 @@ describe("sources fetch", () => {
{
type: FETCH_SOURCES_SUCCESS,
itemId: "scm/core/abc/src",
payload: collection
payload: { updatePending: false, sources: collection }
}
];
@@ -182,14 +180,14 @@ describe("reducer tests", () => {
it("should store the collection, without revision and path", () => {
const expectedState = {
"scm/core/_/": collection
"scm/core/_/": { updatePending: false, sources: collection }
};
expect(reducer({}, fetchSourcesSuccess(repository, "", "", collection))).toEqual(expectedState);
});
it("should store the collection, with revision and path", () => {
const expectedState = {
"scm/core/abc/src/main": collection
"scm/core/abc/src/main": { updatePending: false, sources: collection }
};
expect(reducer({}, fetchSourcesSuccess(repository, "abc", "src/main", collection))).toEqual(expectedState);
});
@@ -200,7 +198,7 @@ describe("selector tests", () => {
const state = {
sources: {
"scm/core/abc/src/main/package.json": {
noDirectory
sources: {noDirectory}
}
}
};
@@ -223,7 +221,9 @@ describe("selector tests", () => {
it("should return the source collection without revision and path", () => {
const state = {
sources: {
"scm/core/_/": collection
"scm/core/_/": {
sources: collection
}
}
};
expect(getSources(state, repository, "", "")).toBe(collection);
@@ -232,7 +232,9 @@ describe("selector tests", () => {
it("should return the source collection with revision and path", () => {
const state = {
sources: {
"scm/core/abc/src/main": collection
"scm/core/abc/src/main": {
sources: collection
}
}
};
expect(getSources(state, repository, "abc", "src/main")).toBe(collection);

View File

@@ -9,13 +9,25 @@ export const FETCH_SOURCES_PENDING = `${FETCH_SOURCES}_${types.PENDING_SUFFIX}`;
export const FETCH_SOURCES_SUCCESS = `${FETCH_SOURCES}_${types.SUCCESS_SUFFIX}`;
export const FETCH_SOURCES_FAILURE = `${FETCH_SOURCES}_${types.FAILURE_SUFFIX}`;
export function fetchSources(repository: Repository, revision: string, path: string) {
return function(dispatch: any) {
dispatch(fetchSourcesPending(repository, revision, path));
export function fetchSources(repository: Repository, revision: string, path: string, initialLoad = true) {
return function(dispatch: any, getState: () => any) {
const state = getState();
if (
isFetchSourcesPending(state, repository, revision, path) ||
isUpdateSourcePending(state, repository, revision, path)
) {
return;
}
if (initialLoad) {
dispatch(fetchSourcesPending(repository, revision, path));
} else {
dispatch(updateSourcesPending(repository, revision, path, getSources(state, repository, revision, path)));
}
return apiClient
.get(createUrl(repository, revision, path))
.then(response => response.json())
.then(sources => {
.then((sources: File) => {
dispatch(fetchSourcesSuccess(repository, revision, path, sources));
})
.catch(err => {
@@ -42,10 +54,23 @@ export function fetchSourcesPending(repository: Repository, revision: string, pa
};
}
export function updateSourcesPending(
repository: Repository,
revision: string,
path: string,
currentSources: any
): Action {
return {
type: "UPDATE_PENDING",
payload: { updatePending: true, sources: currentSources },
itemId: createItemId(repository, revision, path)
};
}
export function fetchSourcesSuccess(repository: Repository, revision: string, path: string, sources: File) {
return {
type: FETCH_SOURCES_SUCCESS,
payload: sources,
payload: { updatePending: false, sources },
itemId: createItemId(repository, revision, path)
};
}
@@ -72,7 +97,7 @@ export default function reducer(
type: "UNKNOWN"
}
): any {
if (action.itemId && action.type === FETCH_SOURCES_SUCCESS) {
if (action.itemId && (action.type === FETCH_SOURCES_SUCCESS || action.type === "UPDATE_PENDING")) {
return {
...state,
[action.itemId]: action.payload
@@ -99,13 +124,17 @@ export function getSources(
path: string
): File | null | undefined {
if (state.sources) {
return state.sources[createItemId(repository, revision, path)];
return state.sources[createItemId(repository, revision, path)]?.sources;
}
return null;
}
export function isFetchSourcesPending(state: any, repository: Repository, revision: string, path: string): boolean {
return isPending(state, FETCH_SOURCES, createItemId(repository, revision, path));
return state && isPending(state, FETCH_SOURCES, createItemId(repository, revision, path));
}
function isUpdateSourcePending(state: any, repository: Repository, revision: string, path: string): boolean {
return state?.sources && state.sources[createItemId(repository, revision, path)]?.updatePending;
}
export function getFetchSourcesFailure(