mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-17 02:31:14 +01:00
implemented navigation within source browser
This commit is contained in:
@@ -7,7 +7,8 @@ type Props = {
|
||||
repository: Repository,
|
||||
to: string,
|
||||
label: string,
|
||||
linkName: string
|
||||
linkName: string,
|
||||
activeOnlyWhenExact: boolean
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -15,13 +16,19 @@ type Props = {
|
||||
*/
|
||||
class RepositoryNavLink extends React.Component<Props> {
|
||||
render() {
|
||||
const { linkName, to, label, repository } = this.props;
|
||||
const { linkName, to, label, repository, activeOnlyWhenExact } = this.props;
|
||||
|
||||
if (!repository._links[linkName]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <NavLink to={to} label={label} />;
|
||||
return (
|
||||
<NavLink
|
||||
to={to}
|
||||
label={label}
|
||||
activeOnlyWhenExact={activeOnlyWhenExact}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
isFetchRepoPending
|
||||
} from "../modules/repos";
|
||||
import { connect } from "react-redux";
|
||||
import { Route } from "react-router-dom";
|
||||
import { Route, withRouter } from "react-router-dom";
|
||||
import type { Repository } from "@scm-manager/ui-types";
|
||||
import {
|
||||
Page,
|
||||
@@ -105,7 +105,24 @@ class RepositoryRoot extends React.Component<Props> {
|
||||
/>
|
||||
<Route
|
||||
path={`${url}/sources`}
|
||||
component={() => <Sources repository={repository} />}
|
||||
exact={true}
|
||||
component={props => (
|
||||
<Sources
|
||||
{...props}
|
||||
repository={repository}
|
||||
baseUrl={`${url}/sources`}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path={`${url}/sources/:revision/:path*`}
|
||||
component={props => (
|
||||
<Sources
|
||||
{...props}
|
||||
repository={repository}
|
||||
baseUrl={`${url}/sources`}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="column">
|
||||
@@ -118,6 +135,7 @@ class RepositoryRoot extends React.Component<Props> {
|
||||
linkName="sources"
|
||||
to={`${url}/sources`}
|
||||
label={t("repository-root.sources")}
|
||||
activeOnlyWhenExact={false}
|
||||
/>
|
||||
</Section>
|
||||
<Section label={t("repository-root.actions-label")}>
|
||||
|
||||
@@ -13,17 +13,40 @@ const styles = {
|
||||
|
||||
type Props = {
|
||||
tree: SourcesCollection,
|
||||
path: string,
|
||||
baseUrl: string,
|
||||
|
||||
// context props
|
||||
classes: any,
|
||||
t: string => string
|
||||
};
|
||||
|
||||
export function findParent(path: string) {
|
||||
if (path.endsWith("/")) {
|
||||
path = path.substring(path, path.length - 1);
|
||||
}
|
||||
|
||||
const index = path.lastIndexOf("/");
|
||||
if (index > 0) {
|
||||
return path.substring(0, index);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
class FileTree extends React.Component<Props> {
|
||||
render() {
|
||||
const { tree, classes, t } = this.props;
|
||||
const { tree, path, baseUrl, classes, t } = this.props;
|
||||
const baseUrlWithRevision = baseUrl + "/" + tree.revision;
|
||||
|
||||
const files = tree._embedded.files;
|
||||
const files = [];
|
||||
if (path) {
|
||||
files.push({
|
||||
name: "..",
|
||||
path: findParent(path),
|
||||
directory: true
|
||||
});
|
||||
}
|
||||
files.push(...tree._embedded.files);
|
||||
|
||||
return (
|
||||
<table className="table table-hover table-sm is-fullwidth">
|
||||
@@ -38,7 +61,11 @@ class FileTree extends React.Component<Props> {
|
||||
</thead>
|
||||
<tbody>
|
||||
{files.map(file => (
|
||||
<FileTreeLeaf key={file.name} file={file} />
|
||||
<FileTreeLeaf
|
||||
key={file.name}
|
||||
file={file}
|
||||
baseUrl={baseUrlWithRevision}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
12
scm-ui/src/repos/sources/components/FileTree.test.js
Normal file
12
scm-ui/src/repos/sources/components/FileTree.test.js
Normal file
@@ -0,0 +1,12 @@
|
||||
// @flow
|
||||
|
||||
import { findParent } from "./FileTree";
|
||||
|
||||
describe("find parent tests", () => {
|
||||
it("should return the parent path", () => {
|
||||
expect(findParent("src/main/js/")).toBe("src/main");
|
||||
expect(findParent("src/main/js")).toBe("src/main");
|
||||
expect(findParent("src/main")).toBe("src");
|
||||
expect(findParent("src")).toBe("");
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import * as React from "react";
|
||||
import injectSheet from "react-jss";
|
||||
import { DateFromNow } from "@scm-manager/ui-components";
|
||||
import FileSize from "./FileSize";
|
||||
@@ -15,25 +15,46 @@ const styles = {
|
||||
|
||||
type Props = {
|
||||
file: File,
|
||||
baseUrl: string,
|
||||
|
||||
// context props
|
||||
classes: any
|
||||
};
|
||||
|
||||
class FileTreeLeaf extends React.Component<Props> {
|
||||
createLink = (file: File) => {
|
||||
let link = this.props.baseUrl;
|
||||
if (file.path) {
|
||||
link += "/" + file.path + "/";
|
||||
}
|
||||
return link;
|
||||
};
|
||||
|
||||
createFileIcon = (file: File) => {
|
||||
if (file.directory) {
|
||||
return (
|
||||
<Link to={this.createLink(file)}>
|
||||
<FileIcon file={file} />
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
return <FileIcon file={file} />;
|
||||
};
|
||||
|
||||
createFileName = (file: File) => {
|
||||
if (file.directory) {
|
||||
return <Link to={this.createLink(file)}>{file.name}</Link>;
|
||||
}
|
||||
return file.name;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { file, classes } = this.props;
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td className={classes.iconColumn}>
|
||||
<Link to="#todo">
|
||||
<FileIcon file={file} />
|
||||
</Link>
|
||||
</td>
|
||||
<td>
|
||||
<Link to="#todo">{file.name}</Link>
|
||||
</td>
|
||||
<td className={classes.iconColumn}>{this.createFileIcon(file)}</td>
|
||||
<td>{this.createFileName(file)}</td>
|
||||
<td>
|
||||
<FileSize bytes={file.length} />
|
||||
</td>
|
||||
|
||||
@@ -16,20 +16,28 @@ type Props = {
|
||||
sources: SourcesCollection,
|
||||
loading: boolean,
|
||||
error: Error,
|
||||
revision: string,
|
||||
path: string,
|
||||
baseUrl: string,
|
||||
|
||||
// dispatch props
|
||||
fetchSources: (repository: Repository) => void
|
||||
fetchSources: (
|
||||
repository: Repository,
|
||||
revision: string,
|
||||
path: string
|
||||
) => void,
|
||||
match: any
|
||||
};
|
||||
|
||||
class Sources extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
const { fetchSources, repository } = this.props;
|
||||
const { fetchSources, repository, revision, path } = this.props;
|
||||
|
||||
fetchSources(repository);
|
||||
fetchSources(repository, revision, path);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { sources, loading, error } = this.props;
|
||||
const { sources, path, baseUrl, loading, error } = this.props;
|
||||
|
||||
if (error) {
|
||||
return <ErrorNotification error={error} />;
|
||||
@@ -39,29 +47,31 @@ class Sources extends React.Component<Props> {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return <FileTree tree={sources} />;
|
||||
return <FileTree tree={sources} path={path} baseUrl={baseUrl} />;
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const { repository } = ownProps;
|
||||
const loading = isFetchSourcesPending(state, repository);
|
||||
const error = getFetchSourcesFailure(state, repository);
|
||||
const sources = getSources(state, repository);
|
||||
const { revision, path } = ownProps.match.params;
|
||||
|
||||
console.log(sources);
|
||||
const loading = isFetchSourcesPending(state, repository, revision, path);
|
||||
const error = getFetchSourcesFailure(state, repository, revision, path);
|
||||
const sources = getSources(state, repository, revision, path);
|
||||
|
||||
return {
|
||||
loading,
|
||||
error,
|
||||
sources
|
||||
sources,
|
||||
revision,
|
||||
path
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
fetchSources: (repository: Repository) => {
|
||||
dispatch(fetchSources(repository));
|
||||
fetchSources: (repository: Repository, revision: string, path: string) => {
|
||||
dispatch(fetchSources(repository, revision, path));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -15,53 +15,78 @@ 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) {
|
||||
export function fetchSources(
|
||||
repository: Repository,
|
||||
revision: string,
|
||||
path: string
|
||||
) {
|
||||
return function(dispatch: any) {
|
||||
dispatch(fetchSourcesPending(repository));
|
||||
dispatch(fetchSourcesPending(repository, revision, path));
|
||||
return apiClient
|
||||
.get(repository._links.sources.href)
|
||||
.get(createUrl(repository, revision, path))
|
||||
.then(response => response.json())
|
||||
.then(sources => {
|
||||
dispatch(fetchSourcesSuccess(repository, sources));
|
||||
dispatch(fetchSourcesSuccess(repository, revision, path, sources));
|
||||
})
|
||||
.catch(err => {
|
||||
const error = new Error(`failed to fetch sources: ${err.message}`);
|
||||
dispatch(fetchSourcesFailure(repository, error));
|
||||
dispatch(fetchSourcesFailure(repository, revision, path, error));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchSourcesPending(repository: Repository): Action {
|
||||
function createUrl(repository: Repository, revision: string, path: string) {
|
||||
const base = repository._links.sources.href;
|
||||
if (!revision && !path) {
|
||||
return base;
|
||||
}
|
||||
|
||||
// TODO handle trailing slash
|
||||
const pathDefined = path ? path : "";
|
||||
return `${base}${revision}/${pathDefined}`;
|
||||
}
|
||||
|
||||
export function fetchSourcesPending(
|
||||
repository: Repository,
|
||||
revision: string,
|
||||
path: string
|
||||
): Action {
|
||||
return {
|
||||
type: FETCH_SOURCES_PENDING,
|
||||
itemId: createItemId(repository)
|
||||
itemId: createItemId(repository, revision, path)
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchSourcesSuccess(
|
||||
repository: Repository,
|
||||
revision: string,
|
||||
path: string,
|
||||
sources: SourcesCollection
|
||||
) {
|
||||
return {
|
||||
type: FETCH_SOURCES_SUCCESS,
|
||||
payload: sources,
|
||||
itemId: createItemId(repository)
|
||||
itemId: createItemId(repository, revision, path)
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchSourcesFailure(
|
||||
repository: Repository,
|
||||
revision: string,
|
||||
path: string,
|
||||
error: Error
|
||||
): Action {
|
||||
return {
|
||||
type: FETCH_SOURCES_FAILURE,
|
||||
payload: error,
|
||||
itemId: createItemId(repository)
|
||||
itemId: createItemId(repository, revision, path)
|
||||
};
|
||||
}
|
||||
|
||||
function createItemId(repository: Repository) {
|
||||
return `${repository.namespace}/${repository.name}`;
|
||||
function createItemId(repository: Repository, revision: string, path: string) {
|
||||
const revPart = revision ? revision : "_";
|
||||
const pathPart = path ? path : "";
|
||||
return `${repository.namespace}/${repository.name}/${revPart}/${pathPart}`;
|
||||
}
|
||||
|
||||
// reducer
|
||||
@@ -83,24 +108,38 @@ export default function reducer(
|
||||
|
||||
export function getSources(
|
||||
state: any,
|
||||
repository: Repository
|
||||
repository: Repository,
|
||||
revision: string,
|
||||
path: string
|
||||
): ?SourcesCollection {
|
||||
if (state.sources) {
|
||||
return state.sources[createItemId(repository)];
|
||||
return state.sources[createItemId(repository, revision, path)];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function isFetchSourcesPending(
|
||||
state: any,
|
||||
repository: Repository
|
||||
repository: Repository,
|
||||
revision: string,
|
||||
path: string
|
||||
): boolean {
|
||||
return isPending(state, FETCH_SOURCES, createItemId(repository));
|
||||
return isPending(
|
||||
state,
|
||||
FETCH_SOURCES,
|
||||
createItemId(repository, revision, path)
|
||||
);
|
||||
}
|
||||
|
||||
export function getFetchSourcesFailure(
|
||||
state: any,
|
||||
repository: Repository
|
||||
repository: Repository,
|
||||
revision: string,
|
||||
path: string
|
||||
): ?Error {
|
||||
return getFailure(state, FETCH_SOURCES, createItemId(repository));
|
||||
return getFailure(
|
||||
state,
|
||||
FETCH_SOURCES,
|
||||
createItemId(repository, revision, path)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
} from "./sources";
|
||||
|
||||
const sourcesUrl =
|
||||
"http://localhost:8081/scm/rest/api/v2/repositories/scm/core/sources";
|
||||
"http://localhost:8081/scm/rest/api/v2/repositories/scm/core/sources/";
|
||||
|
||||
const repository: Repository = {
|
||||
name: "core",
|
||||
@@ -91,10 +91,10 @@ describe("sources fetch", () => {
|
||||
fetchMock.getOnce(sourcesUrl, collection);
|
||||
|
||||
const expectedActions = [
|
||||
{ type: FETCH_SOURCES_PENDING, itemId: "scm/core" },
|
||||
{ type: FETCH_SOURCES_PENDING, itemId: "scm/core/_/" },
|
||||
{
|
||||
type: FETCH_SOURCES_SUCCESS,
|
||||
itemId: "scm/core",
|
||||
itemId: "scm/core/_/",
|
||||
payload: collection
|
||||
}
|
||||
];
|
||||
@@ -105,6 +105,24 @@ describe("sources fetch", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should fetch the sources of the repository with the given revision and path", () => {
|
||||
fetchMock.getOnce(sourcesUrl + "abc/src", collection);
|
||||
|
||||
const expectedActions = [
|
||||
{ type: FETCH_SOURCES_PENDING, itemId: "scm/core/abc/src" },
|
||||
{
|
||||
type: FETCH_SOURCES_SUCCESS,
|
||||
itemId: "scm/core/abc/src",
|
||||
payload: collection
|
||||
}
|
||||
];
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchSources(repository, "abc", "src")).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
it("should dispatch FETCH_SOURCES_FAILURE on server error", () => {
|
||||
fetchMock.getOnce(sourcesUrl, {
|
||||
status: 500
|
||||
@@ -115,7 +133,7 @@ describe("sources fetch", () => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toBe(FETCH_SOURCES_PENDING);
|
||||
expect(actions[1].type).toBe(FETCH_SOURCES_FAILURE);
|
||||
expect(actions[1].itemId).toBe("scm/core");
|
||||
expect(actions[1].itemId).toBe("scm/core/_/");
|
||||
expect(actions[1].payload).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -127,13 +145,25 @@ describe("reducer tests", () => {
|
||||
expect(reducer(state)).toBe(state);
|
||||
});
|
||||
|
||||
it("should store the collection", () => {
|
||||
it("should store the collection, without revision and path", () => {
|
||||
const expectedState = {
|
||||
"scm/core": collection
|
||||
"scm/core/_/": collection
|
||||
};
|
||||
expect(reducer({}, fetchSourcesSuccess(repository, collection))).toEqual(
|
||||
expectedState
|
||||
);
|
||||
expect(
|
||||
reducer({}, fetchSourcesSuccess(repository, null, null, collection))
|
||||
).toEqual(expectedState);
|
||||
});
|
||||
|
||||
it("should store the collection, with revision and path", () => {
|
||||
const expectedState = {
|
||||
"scm/core/abc/src/main": collection
|
||||
};
|
||||
expect(
|
||||
reducer(
|
||||
{},
|
||||
fetchSourcesSuccess(repository, "abc", "src/main", collection)
|
||||
)
|
||||
).toEqual(expectedState);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -142,19 +172,28 @@ describe("selector tests", () => {
|
||||
expect(getSources({}, repository)).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should return the source collection", () => {
|
||||
it("should return the source collection without revision and path", () => {
|
||||
const state = {
|
||||
sources: {
|
||||
"scm/core": collection
|
||||
"scm/core/_/": collection
|
||||
}
|
||||
};
|
||||
expect(getSources(state, repository)).toBe(collection);
|
||||
});
|
||||
|
||||
it("should return the source collection without revision and path", () => {
|
||||
const state = {
|
||||
sources: {
|
||||
"scm/core/abc/src/main": collection
|
||||
}
|
||||
};
|
||||
expect(getSources(state, repository, "abc", "src/main")).toBe(collection);
|
||||
});
|
||||
|
||||
it("should return true, when fetch sources is pending", () => {
|
||||
const state = {
|
||||
pending: {
|
||||
[FETCH_SOURCES + "/scm/core"]: true
|
||||
[FETCH_SOURCES + "/scm/core/_/"]: true
|
||||
}
|
||||
};
|
||||
expect(isFetchSourcesPending(state, repository)).toEqual(true);
|
||||
@@ -169,7 +208,7 @@ describe("selector tests", () => {
|
||||
it("should return error when fetch sources did fail", () => {
|
||||
const state = {
|
||||
failure: {
|
||||
[FETCH_SOURCES + "/scm/core"]: error
|
||||
[FETCH_SOURCES + "/scm/core/_/"]: error
|
||||
}
|
||||
};
|
||||
expect(getFetchSourcesFailure(state, repository)).toEqual(error);
|
||||
|
||||
Reference in New Issue
Block a user