Added types, components and logic for changesets

This commit is contained in:
Philipp Czora
2018-09-11 17:20:30 +02:00
parent 115fa4fb52
commit 9e489cfd1d
9 changed files with 224 additions and 33 deletions

View File

@@ -0,0 +1,18 @@
//@flow
import type { Links } from "./hal";
import type { Tag } from "./Tags";
export type Changeset = {
id: string,
date: Date,
author: {
name: string,
mail: string
},
description: string
_links: Links,
_embedded: {
tags: Tag[]
branches: any, //todo: Add correct type
parents: any //todo: Add correct type
};
}

View File

@@ -0,0 +1,8 @@
//@flow
import type { Links } from "./hal";
export type Tag = {
name: string,
revision: string,
_links: Links
}

View File

@@ -9,4 +9,8 @@ export type { Group, Member } from "./Group";
export type { Repository, RepositoryCollection, RepositoryGroup } from "./Repositories"; export type { Repository, RepositoryCollection, RepositoryGroup } from "./Repositories";
export type { RepositoryType, RepositoryTypeCollection } from "./RepositoryTypes"; export type { RepositoryType, RepositoryTypeCollection } from "./RepositoryTypes";
export type { Changeset } from "./Changesets";
export type { Tag } from "./Tags"
export type { Config } from "./Config"; export type { Config } from "./Config";

View File

@@ -0,0 +1,12 @@
{
"changeset": {
"id": "ID",
"description": "Description",
"contact": "Contact",
"date": "Date"
},
"author": {
"name": "Author",
"mail": "Mail"
}
}

View File

@@ -0,0 +1,21 @@
import React from "react"
import type { Changeset } from "@scm-manager/ui-types"
type Props = {
changeset: Changeset
}
class ChangesetRow extends React.Component<Props> {
// Todo: Add extension point to author field
render() {
const {changeset} = this.props;
return <tr>
<td>{ changeset.author.name }</td>
<td>{ changeset.description }</td>
<td>{ changeset.date }</td>
</tr>
}
}
export default ChangesetRow;

View File

@@ -0,0 +1,59 @@
import React from "react"
import { connect } from "react-redux";
import ChangesetRow from "./ChangesetRow";
import type {Changeset} from "@scm-manager/ui-types";
import { fetchChangesetsByNamespaceAndName, getChangesetsForNameAndNamespaceFromState } from "../modules/changesets";
import { translate } from "react-i18next";
type Props = {
changesets: Changeset[],
t: string => string
}
class Changesets extends React.Component<Props> {
componentDidMount() {
const {namespace, name} = this.props.repository;
this.props.fetchChangesetsByNamespaceAndName(namespace, name);
}
render() {
const { t, changesets } = this.props;
if (!changesets) {
return null;
}
return <table className="table is-hoverable is-fullwidth">
<thead>
<tr>
<th>{t("author.name")}</th>
<th>{t("changeset.description")}</th>
<th className="is-hidden-mobile">{t("changeset.date")}</th>
</tr>
</thead>
<tbody>
{changesets.map((changeset, index) => {
return <ChangesetRow key={index} changeset={changeset} />;
})}
</tbody>
</table>
}
}
const mapStateToProps = (state, ownProps) => {
return {
changesets: getChangesetsForNameAndNamespaceFromState(ownProps.repository.namespace, ownProps.repository.name, state)
}
};
const mapDispatchToProps = dispatch => {
return {
fetchChangesetsByNamespaceAndName: (namespace: string, name: string) => {
dispatch(fetchChangesetsByNamespaceAndName(namespace, name))
}
}
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(translate("changesets")(Changesets));

View File

@@ -10,62 +10,85 @@ export const FETCH_CHANGESETS_FAILURE = `${FETCH_CHANGESETS}_${FAILURE_SUFFIX}`;
const REPO_URL = "repositories"; const REPO_URL = "repositories";
export function fetchChangesets(namespace: string, name: string) {
return fetchChangesetsByLink(REPO_URL + "/" + namespace + "/" + name + "/changesets");
}
export function fetchChangesetsByLink(link: string) { // actions
export function fetchChangesetsByNamespaceAndName(namespace: string, name: string) {
return function (dispatch: any) { return function (dispatch: any) {
dispatch(fetchChangesetsPending()); dispatch(fetchChangesetsPending(namespace, name));
return apiClient.get(link).then(response => response.json()) return apiClient.get(REPO_URL + "/" + namespace + "/" + name + "/changesets").then(response => response.json())
.then(data => { .then(data => {
dispatch(fetchChangesetsSuccess(data)) dispatch(fetchChangesetsSuccess(data, namespace, name))
}).catch(cause => { }).catch(cause => {
dispatch(fetchChangesetsFailure(link, cause)) dispatch(fetchChangesetsFailure(link, cause))
}) })
} }
} }
export function fetchChangesetsPending(): Action { export function fetchChangesetsPending(namespace: string, name: string): Action {
return { return {
type: FETCH_CHANGESETS_PENDING type: FETCH_CHANGESETS_PENDING,
payload: {
namespace,
name
}
} }
} }
export function fetchChangesetsSuccess(data: any): Action { export function fetchChangesetsSuccess(collection: any, namespace: string, name: string): Action {
return { return {
type: FETCH_CHANGESETS_SUCCESS, type: FETCH_CHANGESETS_SUCCESS,
payload: data payload: {collection, namespace, name}
} }
} }
function fetchChangesetsFailure(url: string, error: Error): Action { function fetchChangesetsFailure(namespace: string, name: string, error: Error): Action {
return { return {
type: FETCH_CHANGESETS_FAILURE, type: FETCH_CHANGESETS_FAILURE,
payload: { payload: {
url, namespace,
name,
error error
} }
} }
} }
// reducer
export default function reducer(state: any = {}, action: any = {}) { export default function reducer(state: any = {}, action: any = {}) {
switch (action.type) { switch (action.type) {
case FETCH_CHANGESETS_SUCCESS: case FETCH_CHANGESETS_SUCCESS:
const {namespace, name} = action.payload;
const key = namespace + "/" + name;
return {byIds: extractChangesetsByIds(action.payload)}; let oldChangesets = {[key]: {}};
if (state[key] !== undefined) {
oldChangesets[key] = state[key]
}
return {[key]: {byId: extractChangesetsByIds(action.payload.collection, oldChangesets[key].byId)}};
default: default:
return state; return state;
} }
} }
function extractChangesetsByIds(data: any) { function extractChangesetsByIds(data: any, oldChangesetsByIds: any) {
const changesets = data._embedded.changesets; const changesets = data._embedded.changesets;
const changesetsByIds = {}; const changesetsByIds = {};
for (let changeset of changesets) { for (let changeset of changesets) {
changesetsByIds[changeset.id] = changeset; changesetsByIds[changeset.id] = changeset;
} }
for (let id in oldChangesetsByIds) {
changesetsByIds[id] = oldChangesetsByIds[id];
}
return changesetsByIds; return changesetsByIds;
} }
//selectors
export function getChangesetsForNameAndNamespaceFromState(namespace: string, name: string, state: any) {
const key = namespace + "/" + name;
if (!state.changesets[key]) {
return null;
}
return Object.values(state.changesets[key].byId);
}

View File

@@ -1,14 +1,13 @@
// @flow // @flow
import configureMockStore from "redux-mock-store"; import configureMockStore from "redux-mock-store";
import thunk from "redux-thunk"; import thunk from "redux-thunk";
import fetchMock from "fetch-mock"; import fetchMock from "fetch-mock";
import { import {
FETCH_CHANGESETS_PENDING, FETCH_CHANGESETS_PENDING,
FETCH_CHANGESETS_SUCCESS, FETCH_CHANGESETS_SUCCESS,
fetchChangesets, fetchChangesetsByNamespaceAndName,
fetchChangesetsSuccess fetchChangesetsSuccess, getChangesetsForNameAndNamespaceFromState
} from "./changesets"; } from "./changesets";
import reducer from "./changesets"; import reducer from "./changesets";
@@ -27,15 +26,15 @@ describe("fetching of changesets", () => {
fetchMock.getOnce(URL, "{}"); fetchMock.getOnce(URL, "{}");
const expectedActions = [ const expectedActions = [
{ type: FETCH_CHANGESETS_PENDING }, {type: FETCH_CHANGESETS_PENDING, payload: {namespace: "foo", name: "bar"}},
{ {
type: FETCH_CHANGESETS_SUCCESS, type: FETCH_CHANGESETS_SUCCESS,
payload: collection payload: {collection, namespace: "foo", name: "bar"}
} }
]; ];
const store = mockStore({}); const store = mockStore({});
return store.dispatch(fetchChangesets("foo", "bar")).then(() => { return store.dispatch(fetchChangesetsByNamespaceAndName("foo", "bar")).then(() => {
expect(store.getActions()).toEqual(expectedActions); expect(store.getActions()).toEqual(expectedActions);
}); });
}) })
@@ -46,8 +45,8 @@ describe("changesets reducer", () => {
_embedded: { _embedded: {
changesets: [ changesets: [
{id: "changeset1", author: {mail: "z@phod.com", name: "zaphod"}}, {id: "changeset1", author: {mail: "z@phod.com", name: "zaphod"}},
{id: "changeset2"}, {id: "changeset2", description: "foo"},
{id: "changeset3"}, {id: "changeset3", description: "bar"},
], ],
_embedded: { _embedded: {
tags: [], tags: [],
@@ -56,11 +55,56 @@ describe("changesets reducer", () => {
} }
} }
}; };
it("should set state correctly", () => {
const newState = reducer({}, fetchChangesetsSuccess(responseBody)); it("should set state to received changesets", () => {
expect(newState.byIds["changeset1"]).toBeDefined(); const newState = reducer({}, fetchChangesetsSuccess(responseBody, "foo", "bar"));
expect(newState.byIds["changeset1"].author.mail).toEqual("z@phod.com"); expect(newState).toBeDefined();
expect(newState.byIds["changeset2"]).toBeDefined(); expect(newState["foo/bar"].byId["changeset1"].author.mail).toEqual("z@phod.com");
expect(newState.byIds["changeset3"]).toBeDefined(); expect(newState["foo/bar"].byId["changeset2"].description).toEqual("foo");
expect(newState["foo/bar"].byId["changeset3"].description).toEqual("bar");
});
it("should not delete existing changesets from state", () => {
const responseBody = {
_embedded: {
changesets: [
{id: "changeset1", author: {mail: "z@phod.com", name: "zaphod"}},
],
_embedded: {
tags: [],
branches: [],
parents: []
}
}
};
const newState = reducer({
"foo/bar": {
byId: {
["changeset2"]: {
id: "changeset2",
author: {mail: "mail@author.com", name: "author"}
}
}
}
}, fetchChangesetsSuccess(responseBody, "foo", "bar"));
expect(newState["foo/bar"].byId["changeset2"]).toBeDefined();
expect(newState["foo/bar"].byId["changeset1"]).toBeDefined();
})
});
describe("changeset selectors", () => {
it("should get all changesets for a given namespace and name", () => {
const state = {
changesets: {
["foo/bar"]: {
byId: {
"id1": {id: "id1"},
"id2": {id: "id2"}
}
}
}
};
const result = getChangesetsForNameAndNamespaceFromState("foo", "bar", state);
expect(result).toContainEqual({id: "id1"})
}) })
}); });

View File

@@ -3,6 +3,7 @@ import React from "react";
import type { Repository } from "@scm-manager/ui-types"; import type { Repository } from "@scm-manager/ui-types";
import RepositoryDetailTable from "./RepositoryDetailTable"; import RepositoryDetailTable from "./RepositoryDetailTable";
import { ExtensionPoint } from "@scm-manager/ui-extensions"; import { ExtensionPoint } from "@scm-manager/ui-extensions";
import Changesets from "../../changesets/components/Changesets";
type Props = { type Props = {
repository: Repository repository: Repository
@@ -20,6 +21,7 @@ class RepositoryDetails extends React.Component<Props> {
renderAll={true} renderAll={true}
props={{ repository }} props={{ repository }}
/> />
<Changesets repository={repository}/>
</div> </div>
</div> </div>
); );