added defaultBranch to Branches type, changed ui-bundler version for better testing experience in intellij, corrected fetchBranch functionality, wrote reducer for single branch and unit tests

This commit is contained in:
Florian Scholdei
2019-04-02 17:31:32 +02:00
parent e736add3fd
commit 7c61efb20b
7 changed files with 112 additions and 76 deletions

View File

@@ -4,5 +4,6 @@ import type {Links} from "./hal";
export type Branch = {
name: string,
revision: string,
defaultBranch?: boolean,
_links: Links
}

View File

@@ -52,7 +52,7 @@
"pre-commit": "jest && flow && eslint src"
},
"devDependencies": {
"@scm-manager/ui-bundler": "^0.0.26",
"@scm-manager/ui-bundler": "^0.0.27",
"concat": "^1.0.3",
"copyfiles": "^2.0.0",
"enzyme": "^3.3.0",

View File

@@ -19,7 +19,7 @@ const styles = {
};
class BranchRow extends React.Component<Props> {
renderLink(to: string, label: string, defaultBranch: boolean) {
renderLink(to: string, label: string, defaultBranch?: boolean) {
const { classes } = this.props;
let showLabel = null;
@@ -36,11 +36,11 @@ class BranchRow extends React.Component<Props> {
render() {
const { baseUrl, branch } = this.props;
const to = `${baseUrl}/${encodeURIComponent(branch.name)}/info`;
return (
<tr>
<td>{this.renderLink(to, branch.name, branch.defaultBranch)}</td>
</tr>
);
return (
<tr>
<td>{this.renderLink(to, branch.name, branch.defaultBranch)}</td>
</tr>
);
}
}

View File

@@ -7,8 +7,8 @@ import { connect } from "react-redux";
import { translate } from "react-i18next";
import { withRouter } from "react-router-dom";
import {
fetchBranchByName,
getBranchByName,
fetchBranch,
getBranch,
getFetchBranchFailure,
isFetchBranchPending
} from "../modules/branches";
@@ -22,7 +22,7 @@ type Props = {
branch: Branch,
// dispatch functions
fetchBranchByName: (repository: Repository, branchName: string) => void,
fetchBranch: (repository: Repository, branchName: string) => void,
// context props
t: string => string
@@ -30,9 +30,9 @@ type Props = {
class BranchView extends React.Component<Props> {
componentDidMount() {
const { fetchBranchByName, repository, branchName } = this.props;
const { fetchBranch, repository, branchName } = this.props;
fetchBranchByName(repository, branchName);
fetchBranch(repository, branchName);
}
render() {
@@ -71,7 +71,7 @@ class BranchView extends React.Component<Props> {
const mapStateToProps = (state, ownProps) => {
const { repository } = ownProps;
const branchName = decodeURIComponent(ownProps.match.params.branch);
const branch = getBranchByName(state, branchName);
const branch = getBranch(state, repository, branchName);
const loading = isFetchBranchPending(state, branchName);
const error = getFetchBranchFailure(state, branchName);
return {
@@ -85,8 +85,8 @@ const mapStateToProps = (state, ownProps) => {
const mapDispatchToProps = dispatch => {
return {
fetchBranchByName: (repository: Repository, branchName: string) => {
dispatch(fetchBranchByName(repository._links.branches.href, branchName));
fetchBranch: (repository: Repository, branchName: string) => {
dispatch(fetchBranch(repository, branchName));
}
};
};

View File

@@ -5,11 +5,7 @@ import {
SUCCESS_SUFFIX
} from "../../../modules/types";
import { apiClient } from "@scm-manager/ui-components";
import type {
Action,
Branch,
Repository
} from "@scm-manager/ui-types";
import type { Action, Branch, Repository } from "@scm-manager/ui-types";
import { isPending } from "../../../modules/pending";
import { getFailure } from "../../../modules/failure";
@@ -25,61 +21,69 @@ export const FETCH_BRANCH_FAILURE = `${FETCH_BRANCH}_${FAILURE_SUFFIX}`;
// Fetching branches
export function fetchBranchByName(link: string, name: string) {
let endsWith = "";
if(!link.endsWith("/")) {
endsWith = "/";
}
return fetchBranch(link + endsWith + encodeURIComponent(name), name);
function createIdentifier(repository: Repository) {
return repository.namespace + "/" + repository.name;
}
export function fetchBranchPending(name: string): Action {
export function fetchBranchPending(
repository: Repository,
name: string
): Action {
return {
type: FETCH_BRANCH_PENDING,
payload: name,
itemId: name
payload: { repository, name },
itemId: createIdentifier(repository) + "/" + name
};
}
export function fetchBranchSuccess(repo: Repository, branch: Branch): Action {
export function fetchBranchSuccess(
repository: Repository,
branch: Branch
): Action {
return {
type: FETCH_BRANCH_SUCCESS,
payload: branch,
itemId: branch.name
payload: { repository, branch },
itemId: createIdentifier(repository) + "/" + branch.name
};
}
export function fetchBranchFailure(name: string, error: Error): Action {
export function fetchBranchFailure(
repository: Repository,
name: string,
error: Error
): Action {
return {
type: FETCH_BRANCH_FAILURE,
payload: name,
itemId: name
payload: { error, repository, name },
itemId: createIdentifier(repository) + "/" + name
};
}
export function fetchBranch(link: string, name: string) {
export function fetchBranch(
repository: Repository,
name: string
) {
let link = repository._links.branches.href;
if (!link.endsWith("/")) {
link += "/";
}
link += encodeURIComponent(name);
return function(dispatch: any) {
dispatch(fetchBranchPending(name));
dispatch(fetchBranchPending(repository, name));
return apiClient
.get(link)
.then(response => {
return response.json();
})
.then(data => {
dispatch(fetchBranchSuccess(data));
dispatch(fetchBranchSuccess(repository, data));
})
.catch(error => {
dispatch(fetchBranchFailure(name, error));
dispatch(fetchBranchFailure(repository, name, error));
});
};
}
export function getBranchByName(state: Object, name: string) {
if (state.branches) {
return state.branches[name];
}
}
export function isFetchBranchPending(state: Object, name: string) {
return isPending(state, FETCH_BRANCH, name);
}
@@ -138,6 +142,24 @@ export function fetchBranchesFailure(repository: Repository, error: Error) {
// Reducers
function reduceBranchSuccess(state, repositoryName, newBranch) {
const newBranches = [];
// we do not use filter, because we try to keep the current order
let found = false;
for (const branch of state[repositoryName] || []) {
if (branch.name === newBranch.name) {
newBranches.push(newBranch);
found = true;
} else {
newBranches.push(branch);
}
}
if (!found) {
newBranches.push(newBranch);
}
return newBranches;
}
type State = { [string]: Branch[] };
export default function reducer(
@@ -156,11 +178,15 @@ export default function reducer(
[key]: extractBranchesFromPayload(payload.data)
};
case FETCH_BRANCH_SUCCESS:
if (!action.payload.repository || !action.payload.branch) {
return state;
}
const newBranch = action.payload.branch;
const repositoryName = createIdentifier(action.payload.repository);
return {
...state,
[action.payload.name]: action.payload
[repositoryName]: reduceBranchSuccess(state, repositoryName, newBranch)
};
default:
return state;
}

View File

@@ -11,9 +11,8 @@ import reducer, {
FETCH_BRANCH_SUCCESS,
FETCH_BRANCH_FAILURE,
fetchBranches,
fetchBranchByName,
fetchBranchSuccess,
fetchBranch,
fetchBranchSuccess,
getBranch,
getBranches,
getFetchBranchesFailure,
@@ -100,7 +99,7 @@ describe("branches", () => {
fetchMock.getOnce(URL + "/branch1", branch1);
const store = mockStore({});
return store.dispatch(fetchBranchByName(URL, "branch1")).then(() => {
return store.dispatch(fetchBranch(repository, "branch1")).then(() => {
const actions = store.getActions();
expect(actions[0].type).toEqual(FETCH_BRANCH_PENDING);
expect(actions[1].type).toEqual(FETCH_BRANCH_SUCCESS);
@@ -114,7 +113,7 @@ describe("branches", () => {
});
const store = mockStore({});
return store.dispatch(fetchBranchByName(URL, "branch2")).then(() => {
return store.dispatch(fetchBranch(repository, "branch2")).then(() => {
const actions = store.getActions();
expect(actions[0].type).toEqual(FETCH_BRANCH_PENDING);
expect(actions[1].type).toEqual(FETCH_BRANCH_FAILURE);
@@ -149,51 +148,60 @@ describe("branches", () => {
const oldState = {
"hitchhiker/heartOfGold": [branch3]
};
const newState = reducer(oldState, action);
expect(newState[key]).toContain(branch1);
expect(newState[key]).toContain(branch2);
expect(newState["hitchhiker/heartOfGold"]).toContain(branch3);
});
it("should update state according to FETCH_BRANCH_SUCCESS action", () => {
const newState = reducer({}, fetchBranchSuccess(branch3));
expect(newState["branch3"]).toBe(branch3);
const newState = reducer({}, fetchBranchSuccess(repository, branch3));
expect(newState["foo/bar"]).toEqual([branch3]);
});
it("should not delete existing branch from state", () => {
const oldState = {
branch1
"foo/bar": [branch1]
};
const newState = reducer(oldState, fetchBranchSuccess(branch2));
expect(newState["branch1"]).toBe(branch1);
expect(newState["branch2"]).toBe(branch2);
const newState = reducer(oldState, fetchBranchSuccess(repository, branch2));
expect(newState["foo/bar"]).toEqual([branch1, branch2]);
});
it("should update required branch from state", () => {
const oldState = {
branch1
"foo/bar": [branch1]
};
const newBranch1 = { name: "branch1", revision: "revision2" };
const newState = reducer(oldState, fetchBranchSuccess(newBranch1));
expect(newState["branch1"]).not.toBe(branch1);
expect(newState["branch1"]).toBe(newBranch1);
const newState = reducer(oldState, fetchBranchSuccess(repository, newBranch1));
expect(newState["foo/bar"]).toEqual([newBranch1]);
});
it("should update required branch from state and keeps old repo", () => {
const oldState = {
repo1: {
branch1
}
"ns/one": [branch1]
};
const repo2 = { repo2: { branch3 } };
const newState = reducer(oldState, fetchBranchSuccess(repo2, branch2));
expect(newState["repo1"]).toBe({ branch1 });
expect(newState["repo2"]).toBe({ branch2, branch3 });
const newState = reducer(oldState, fetchBranchSuccess(repository, branch3));
expect(newState["ns/one"]).toEqual([branch1]);
expect(newState["foo/bar"]).toEqual([branch3]);
});
it("should return the oldState, if action has no payload", () => {
const state = {};
const newState = reducer(state, {type: FETCH_BRANCH_SUCCESS});
expect(newState).toBe(state);
});
it("should return the oldState, if payload has no branch", () => {
const action = {
type: FETCH_BRANCH_SUCCESS,
payload: {
repository
},
itemId: "foo/bar/"
};
const state = {};
const newState = reducer(state, action);
expect(newState).toBe(state);
});
});

View File

@@ -698,9 +698,10 @@
version "0.0.2"
resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85"
"@scm-manager/ui-bundler@^0.0.26":
version "0.0.26"
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.26.tgz#4676a7079b781b33fa1989c6643205c3559b1f66"
"@scm-manager/ui-bundler@^0.0.27":
version "0.0.27"
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.27.tgz#3ed2c7826780b9a1a9ea90464332640cfb5d54b5"
integrity sha512-cBU1xq6gDy1Vw9AGOzsR763+JmBeraTaC/KQfxT3I6XyZJ2brIfG1m5QYcAcHWvDxq3mYMogpI5rfShw14L4/w==
dependencies:
"@babel/core" "^7.0.0"
"@babel/plugin-proposal-class-properties" "^7.0.0"