mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-01 19:15:52 +01:00
start implementation repository details
This commit is contained in:
@@ -2,5 +2,9 @@
|
||||
"overview": {
|
||||
"title": "Repositories",
|
||||
"subtitle": "Overview of available repositories"
|
||||
},
|
||||
"repository-root": {
|
||||
"error-title": "Error",
|
||||
"error-subtitle": "Unknown repository error"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Switch } from "react-router-dom";
|
||||
import ProtectedRoute from "../components/ProtectedRoute";
|
||||
import AddUser from "../users/containers/AddUser";
|
||||
import SingleUser from "../users/containers/SingleUser";
|
||||
import RepositoryRoot from '../repos/containers/RepositoryRoot';
|
||||
|
||||
type Props = {
|
||||
authenticated?: boolean
|
||||
@@ -38,6 +39,12 @@ class Main extends React.Component<Props> {
|
||||
component={Overview}
|
||||
authenticated={authenticated}
|
||||
/>
|
||||
<ProtectedRoute
|
||||
exact
|
||||
path="/repo/:namespace/:name"
|
||||
component={RepositoryRoot}
|
||||
authenticated={authenticated}
|
||||
/>
|
||||
<ProtectedRoute
|
||||
exact
|
||||
path="/users"
|
||||
|
||||
16
scm-ui/src/repos/components/RepositoryDetails.js
Normal file
16
scm-ui/src/repos/components/RepositoryDetails.js
Normal file
@@ -0,0 +1,16 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import type {Repository} from '../types/Repositories';
|
||||
|
||||
type Props = {
|
||||
repository: Repository
|
||||
};
|
||||
|
||||
class RepositoryDetails extends React.Component<Props> {
|
||||
render() {
|
||||
const { repository } = this.props;
|
||||
return <div>{repository.description}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
export default RepositoryDetails;
|
||||
108
scm-ui/src/repos/containers/RepositoryRoot.js
Normal file
108
scm-ui/src/repos/containers/RepositoryRoot.js
Normal file
@@ -0,0 +1,108 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import {fetchRepo, getFetchRepoFailure, getRepository, isFetchRepoPending} from '../modules/repos';
|
||||
import { connect } from "react-redux";
|
||||
import {Route} from "react-router-dom"
|
||||
import type {Repository} from '../types/Repositories';
|
||||
import {Page} from '../../components/layout';
|
||||
import Loading from '../../components/Loading';
|
||||
import ErrorPage from '../../components/ErrorPage';
|
||||
import { translate } from "react-i18next";
|
||||
import {Navigation} from '../../components/navigation';
|
||||
import RepositoryDetails from '../components/RepositoryDetails';
|
||||
|
||||
type Props = {
|
||||
namespace: string,
|
||||
name: string,
|
||||
repository: Repository,
|
||||
loading: boolean,
|
||||
error: Error,
|
||||
|
||||
// dispatch functions
|
||||
fetchRepo: (namespace: string, name: string) => void,
|
||||
|
||||
// context props
|
||||
t: string => string,
|
||||
match: any
|
||||
};
|
||||
|
||||
class RepositoryRoot extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
const { fetchRepo, namespace, name } = this.props;
|
||||
|
||||
fetchRepo(namespace, name);
|
||||
}
|
||||
|
||||
stripEndingSlash = (url: string) => {
|
||||
if (url.endsWith("/")) {
|
||||
return url.substring(0, url.length - 2);
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
matchedUrl = () => {
|
||||
return this.stripEndingSlash(this.props.match.url);
|
||||
};
|
||||
|
||||
|
||||
render() {
|
||||
const { loading, error, repository, t } = this.props;
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<ErrorPage
|
||||
title={t("repository-root.error-title")}
|
||||
subtitle={t("repository-root.error-subtitle")}
|
||||
error={error}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!repository || loading) {
|
||||
return <Loading />
|
||||
}
|
||||
|
||||
const url = this.matchedUrl();
|
||||
|
||||
return <Page title={repository.namespace + "/" + repository.name}>
|
||||
<div className="columns">
|
||||
<div className="column is-three-quarters">
|
||||
<Route path={url} exact component={() => <RepositoryDetails repository={repository} />} />
|
||||
</div>
|
||||
<div className="column">
|
||||
<Navigation>
|
||||
|
||||
</Navigation>
|
||||
</div>
|
||||
</div>
|
||||
</Page>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const { namespace, name } = ownProps.match.params;
|
||||
const repository = getRepository(state, namespace, name);
|
||||
const loading = isFetchRepoPending(state, namespace, name);
|
||||
const error = getFetchRepoFailure(state, namespace, name);
|
||||
return {
|
||||
namespace,
|
||||
name,
|
||||
repository,
|
||||
loading,
|
||||
error
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
fetchRepo : (namespace: string, name: string) => {
|
||||
dispatch(fetchRepo(namespace, name))
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(translate("repos")(RepositoryRoot));
|
||||
@@ -2,7 +2,7 @@
|
||||
import { apiClient } from "../../apiclient";
|
||||
import * as types from "../../modules/types";
|
||||
import type { Action } from "../../types/Action";
|
||||
import type { RepositoryCollection } from "../types/Repositories";
|
||||
import type {Repository, RepositoryCollection} from "../types/Repositories";
|
||||
import {isPending} from "../../modules/pending";
|
||||
import {getFailure} from "../../modules/failure";
|
||||
|
||||
@@ -11,7 +11,15 @@ export const FETCH_REPOS_PENDING = `${FETCH_REPOS}_${types.PENDING_SUFFIX}`;
|
||||
export const FETCH_REPOS_SUCCESS = `${FETCH_REPOS}_${types.SUCCESS_SUFFIX}`;
|
||||
export const FETCH_REPOS_FAILURE = `${FETCH_REPOS}_${types.FAILURE_SUFFIX}`;
|
||||
|
||||
export const FETCH_REPO = "scm/repos/FETCH_REPO";
|
||||
export const FETCH_REPO_PENDING = `${FETCH_REPO}_${types.PENDING_SUFFIX}`;
|
||||
export const FETCH_REPO_SUCCESS = `${FETCH_REPO}_${types.SUCCESS_SUFFIX}`;
|
||||
export const FETCH_REPO_FAILURE = `${FETCH_REPO}_${types.FAILURE_SUFFIX}`;
|
||||
|
||||
const REPOS_URL = "repositories";
|
||||
|
||||
// fetch repos
|
||||
|
||||
const SORT_BY = "sortBy=namespaceAndName";
|
||||
|
||||
export function fetchRepos() {
|
||||
@@ -71,15 +79,66 @@ export function fetchReposFailure(err: Error): Action {
|
||||
};
|
||||
}
|
||||
|
||||
// fetch repo
|
||||
|
||||
export function fetchRepo(namespace: string, name: string) {
|
||||
return function(dispatch: any) {
|
||||
dispatch(fetchRepoPending(namespace, name));
|
||||
return apiClient.get(`${REPOS_URL}/${namespace}/${name}`)
|
||||
.then(response => response.json())
|
||||
.then( repository => {
|
||||
dispatch(fetchRepoSuccess(repository))
|
||||
} )
|
||||
.catch(err => {
|
||||
dispatch(fetchRepoFailure(namespace, name, err))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function fetchRepoPending(namespace: string, name: string): Action {
|
||||
return {
|
||||
type: FETCH_REPO_PENDING,
|
||||
payload: {
|
||||
namespace,
|
||||
name
|
||||
},
|
||||
itemId: namespace + "/" + name
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchRepoSuccess(repository: Repository): Action {
|
||||
return {
|
||||
type: FETCH_REPO_SUCCESS,
|
||||
payload: repository,
|
||||
itemId: createIdentifier(repository)
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchRepoFailure(namespace: string, name: string, error: Error): Action {
|
||||
return {
|
||||
type: FETCH_REPO_FAILURE,
|
||||
payload: {
|
||||
namespace,
|
||||
name,
|
||||
error
|
||||
},
|
||||
itemId: namespace + "/" + name
|
||||
};
|
||||
}
|
||||
|
||||
// reducer
|
||||
|
||||
function createIdentifier(repository: Repository) {
|
||||
return repository.namespace + "/" + repository.name;
|
||||
}
|
||||
|
||||
function normalizeByNamespaceAndName(
|
||||
repositoryCollection: RepositoryCollection
|
||||
) {
|
||||
const names = [];
|
||||
const byNames = {};
|
||||
for (const repository of repositoryCollection._embedded.repositories) {
|
||||
const identifier = repository.namespace + "/" + repository.name;
|
||||
const identifier = createIdentifier(repository);
|
||||
names.push(identifier);
|
||||
byNames[identifier] = repository;
|
||||
}
|
||||
@@ -94,18 +153,33 @@ function normalizeByNamespaceAndName(
|
||||
};
|
||||
}
|
||||
|
||||
const reducerByNames = (state: Object, repository: Repository) => {
|
||||
const identifier = createIdentifier(repository);
|
||||
const newState = {
|
||||
...state,
|
||||
byNames: {
|
||||
...state.byNames,
|
||||
[identifier]: repository
|
||||
}
|
||||
};
|
||||
|
||||
return newState;
|
||||
};
|
||||
|
||||
export default function reducer(
|
||||
state: Object = {},
|
||||
action: Action = { type: "UNKNOWN" }
|
||||
): Object {
|
||||
if (action.type === FETCH_REPOS_SUCCESS) {
|
||||
if (action.payload) {
|
||||
return normalizeByNamespaceAndName(action.payload);
|
||||
} else {
|
||||
// TODO ???
|
||||
if (!action.payload) {
|
||||
return state;
|
||||
}
|
||||
} else {
|
||||
|
||||
switch (action.type) {
|
||||
case FETCH_REPOS_SUCCESS:
|
||||
return normalizeByNamespaceAndName(action.payload);
|
||||
case FETCH_REPO_SUCCESS:
|
||||
return reducerByNames(state, action.payload);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -134,3 +208,17 @@ export function isFetchReposPending(state: Object) {
|
||||
export function getFetchReposFailure(state: Object) {
|
||||
return getFailure(state, FETCH_REPOS);
|
||||
}
|
||||
|
||||
export function getRepository(state: Object, namespace: string, name: string) {
|
||||
if (state.repos && state.repos.byNames) {
|
||||
return state.repos.byNames[ namespace + "/" + name];
|
||||
}
|
||||
}
|
||||
|
||||
export function isFetchRepoPending(state: Object, namespace: string, name: string) {
|
||||
return isPending(state, FETCH_REPO, namespace + "/" + name);
|
||||
}
|
||||
|
||||
export function getFetchRepoFailure(state: Object, namespace: string, name: string) {
|
||||
return getFailure(state, FETCH_REPO, namespace + "/" + name);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,16 @@ import reducer, {
|
||||
isFetchReposPending,
|
||||
getFetchReposFailure,
|
||||
fetchReposByLink,
|
||||
fetchReposByPage
|
||||
fetchReposByPage,
|
||||
FETCH_REPO,
|
||||
fetchRepo,
|
||||
FETCH_REPO_PENDING,
|
||||
FETCH_REPO_SUCCESS,
|
||||
FETCH_REPO_FAILURE,
|
||||
fetchRepoSuccess,
|
||||
getRepository,
|
||||
isFetchRepoPending,
|
||||
getFetchRepoFailure
|
||||
} from "./repos";
|
||||
import type { Repository, RepositoryCollection } from "../types/Repositories";
|
||||
|
||||
@@ -199,7 +208,9 @@ const repositoryCollectionWithNames: RepositoryCollection = {
|
||||
};
|
||||
|
||||
describe("repos fetch", () => {
|
||||
const REPOS_URL = "/scm/api/rest/v2/repositories?sortBy=namespaceAndName";
|
||||
const REPOS_URL = "/scm/api/rest/v2/repositories";
|
||||
const SORT = "sortBy=namespaceAndName";
|
||||
const REPOS_URL_WITH_SORT = REPOS_URL + "?" + SORT;
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
|
||||
afterEach(() => {
|
||||
@@ -208,8 +219,7 @@ describe("repos fetch", () => {
|
||||
});
|
||||
|
||||
it("should successfully fetch repos", () => {
|
||||
const url = REPOS_URL + "&page=42";
|
||||
fetchMock.getOnce(url, repositoryCollection);
|
||||
fetchMock.getOnce(REPOS_URL_WITH_SORT, repositoryCollection);
|
||||
|
||||
const expectedActions = [
|
||||
{ type: FETCH_REPOS_PENDING },
|
||||
@@ -226,7 +236,7 @@ describe("repos fetch", () => {
|
||||
});
|
||||
|
||||
it("should successfully fetch page 42", () => {
|
||||
const url = REPOS_URL + "&page=42";
|
||||
const url = REPOS_URL + "?page=42&" + SORT;
|
||||
fetchMock.getOnce(url, repositoryCollection);
|
||||
|
||||
const expectedActions = [
|
||||
@@ -245,7 +255,10 @@ describe("repos fetch", () => {
|
||||
});
|
||||
|
||||
it("should successfully fetch repos from link", () => {
|
||||
fetchMock.getOnce(REPOS_URL, repositoryCollection);
|
||||
fetchMock.getOnce(
|
||||
REPOS_URL + "?" + SORT + "&page=42",
|
||||
repositoryCollection
|
||||
);
|
||||
|
||||
const expectedActions = [
|
||||
{ type: FETCH_REPOS_PENDING },
|
||||
@@ -287,7 +300,7 @@ describe("repos fetch", () => {
|
||||
});
|
||||
|
||||
it("should dispatch FETCH_REPOS_FAILURE, it the request fails", () => {
|
||||
fetchMock.getOnce(REPOS_URL, {
|
||||
fetchMock.getOnce(REPOS_URL_WITH_SORT, {
|
||||
status: 500
|
||||
});
|
||||
|
||||
@@ -299,6 +312,48 @@ describe("repos fetch", () => {
|
||||
expect(actions[1].payload).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("should successfully fetch repo slarti/fjords", () => {
|
||||
fetchMock.getOnce(REPOS_URL + "/slarti/fjords", slartiFjords);
|
||||
|
||||
const expectedActions = [
|
||||
{
|
||||
type: FETCH_REPO_PENDING,
|
||||
payload: {
|
||||
namespace: "slarti",
|
||||
name: "fjords"
|
||||
},
|
||||
itemId: "slarti/fjords"
|
||||
},
|
||||
{
|
||||
type: FETCH_REPO_SUCCESS,
|
||||
payload: slartiFjords,
|
||||
itemId: "slarti/fjords"
|
||||
}
|
||||
];
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchRepo("slarti", "fjords")).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
it("should dispatch FETCH_REPO_FAILURE, it the request for slarti/fjords fails", () => {
|
||||
fetchMock.getOnce(REPOS_URL + "/slarti/fjords", {
|
||||
status: 500
|
||||
});
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchRepo("slarti", "fjords")).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(FETCH_REPO_PENDING);
|
||||
expect(actions[1].type).toEqual(FETCH_REPO_FAILURE);
|
||||
expect(actions[1].payload.namespace).toBe("slarti");
|
||||
expect(actions[1].payload.name).toBe("fjords");
|
||||
expect(actions[1].payload.error).toBeDefined();
|
||||
expect(actions[1].itemId).toBe("slarti/fjords");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("repos reducer", () => {
|
||||
@@ -330,6 +385,11 @@ describe("repos reducer", () => {
|
||||
expect(newState.byNames["hitchhiker/restatend"]).toBe(hitchhikerRestatend);
|
||||
expect(newState.byNames["slarti/fjords"]).toBe(slartiFjords);
|
||||
});
|
||||
|
||||
it("should store the repo at byNames", () => {
|
||||
const newState = reducer({}, fetchRepoSuccess(slartiFjords));
|
||||
expect(newState.byNames["slarti/fjords"]).toBe(slartiFjords);
|
||||
});
|
||||
});
|
||||
|
||||
describe("repos selectors", () => {
|
||||
@@ -376,4 +436,39 @@ describe("repos selectors", () => {
|
||||
it("should return undefined when fetch repos did not fail", () => {
|
||||
expect(getFetchReposFailure({})).toBe(undefined);
|
||||
});
|
||||
|
||||
it("should return the repository collection", () => {
|
||||
const state = {
|
||||
repos: {
|
||||
byNames: {
|
||||
"slarti/fjords": slartiFjords
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const repository = getRepository(state, "slarti", "fjords");
|
||||
expect(repository).toEqual(slartiFjords);
|
||||
});
|
||||
|
||||
it("should return true, when fetch repo is pending", () => {
|
||||
const state = {
|
||||
pending: {
|
||||
[FETCH_REPO + "/slarti/fjords"]: true
|
||||
}
|
||||
};
|
||||
expect(isFetchRepoPending(state, "slarti", "fjords")).toEqual(true);
|
||||
});
|
||||
|
||||
it("should return false, when fetch repo is not pending", () => {
|
||||
expect(isFetchRepoPending({}, "slarti", "fjords")).toEqual(false);
|
||||
});
|
||||
|
||||
it("should return error when fetch repo did fail", () => {
|
||||
const state = {
|
||||
failure: {
|
||||
[FETCH_REPO + "/slarti/fjords"]: error
|
||||
}
|
||||
};
|
||||
expect(getFetchRepoFailure(state, "slarti", "fjords")).toEqual(error);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -52,7 +52,7 @@ class Users extends React.Component<Props> {
|
||||
*/
|
||||
componentDidUpdate() {
|
||||
const { page, list } = this.props;
|
||||
if (list && list.page || list.page === 0) {
|
||||
if (list && (list.page || list.page === 0)) {
|
||||
// backend starts paging by 0
|
||||
const statePage: number = list.page + 1;
|
||||
if (page !== statePage) {
|
||||
|
||||
Reference in New Issue
Block a user