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 = { export type Branch = {
name: string, name: string,
revision: string, revision: string,
defaultBranch?: boolean,
_links: Links _links: Links
} }

View File

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

View File

@@ -19,7 +19,7 @@ const styles = {
}; };
class BranchRow extends React.Component<Props> { class BranchRow extends React.Component<Props> {
renderLink(to: string, label: string, defaultBranch: boolean) { renderLink(to: string, label: string, defaultBranch?: boolean) {
const { classes } = this.props; const { classes } = this.props;
let showLabel = null; let showLabel = null;

View File

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

View File

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

View File

@@ -11,9 +11,8 @@ import reducer, {
FETCH_BRANCH_SUCCESS, FETCH_BRANCH_SUCCESS,
FETCH_BRANCH_FAILURE, FETCH_BRANCH_FAILURE,
fetchBranches, fetchBranches,
fetchBranchByName,
fetchBranchSuccess,
fetchBranch, fetchBranch,
fetchBranchSuccess,
getBranch, getBranch,
getBranches, getBranches,
getFetchBranchesFailure, getFetchBranchesFailure,
@@ -100,7 +99,7 @@ describe("branches", () => {
fetchMock.getOnce(URL + "/branch1", branch1); fetchMock.getOnce(URL + "/branch1", branch1);
const store = mockStore({}); const store = mockStore({});
return store.dispatch(fetchBranchByName(URL, "branch1")).then(() => { return store.dispatch(fetchBranch(repository, "branch1")).then(() => {
const actions = store.getActions(); const actions = store.getActions();
expect(actions[0].type).toEqual(FETCH_BRANCH_PENDING); expect(actions[0].type).toEqual(FETCH_BRANCH_PENDING);
expect(actions[1].type).toEqual(FETCH_BRANCH_SUCCESS); expect(actions[1].type).toEqual(FETCH_BRANCH_SUCCESS);
@@ -114,7 +113,7 @@ describe("branches", () => {
}); });
const store = mockStore({}); const store = mockStore({});
return store.dispatch(fetchBranchByName(URL, "branch2")).then(() => { return store.dispatch(fetchBranch(repository, "branch2")).then(() => {
const actions = store.getActions(); const actions = store.getActions();
expect(actions[0].type).toEqual(FETCH_BRANCH_PENDING); expect(actions[0].type).toEqual(FETCH_BRANCH_PENDING);
expect(actions[1].type).toEqual(FETCH_BRANCH_FAILURE); expect(actions[1].type).toEqual(FETCH_BRANCH_FAILURE);
@@ -149,51 +148,60 @@ describe("branches", () => {
const oldState = { const oldState = {
"hitchhiker/heartOfGold": [branch3] "hitchhiker/heartOfGold": [branch3]
}; };
const newState = reducer(oldState, action); const newState = reducer(oldState, action);
expect(newState[key]).toContain(branch1); expect(newState[key]).toContain(branch1);
expect(newState[key]).toContain(branch2); expect(newState[key]).toContain(branch2);
expect(newState["hitchhiker/heartOfGold"]).toContain(branch3); expect(newState["hitchhiker/heartOfGold"]).toContain(branch3);
}); });
it("should update state according to FETCH_BRANCH_SUCCESS action", () => { it("should update state according to FETCH_BRANCH_SUCCESS action", () => {
const newState = reducer({}, fetchBranchSuccess(branch3)); const newState = reducer({}, fetchBranchSuccess(repository, branch3));
expect(newState["branch3"]).toBe(branch3); expect(newState["foo/bar"]).toEqual([branch3]);
}); });
it("should not delete existing branch from state", () => { it("should not delete existing branch from state", () => {
const oldState = { const oldState = {
branch1 "foo/bar": [branch1]
}; };
const newState = reducer(oldState, fetchBranchSuccess(repository, branch2));
const newState = reducer(oldState, fetchBranchSuccess(branch2)); expect(newState["foo/bar"]).toEqual([branch1, branch2]);
expect(newState["branch1"]).toBe(branch1);
expect(newState["branch2"]).toBe(branch2);
}); });
it("should update required branch from state", () => { it("should update required branch from state", () => {
const oldState = { const oldState = {
branch1 "foo/bar": [branch1]
}; };
const newBranch1 = { name: "branch1", revision: "revision2" }; const newBranch1 = { name: "branch1", revision: "revision2" };
const newState = reducer(oldState, fetchBranchSuccess(repository, newBranch1));
const newState = reducer(oldState, fetchBranchSuccess(newBranch1)); expect(newState["foo/bar"]).toEqual([newBranch1]);
expect(newState["branch1"]).not.toBe(branch1);
expect(newState["branch1"]).toBe(newBranch1);
}); });
it("should update required branch from state and keeps old repo", () => { it("should update required branch from state and keeps old repo", () => {
const oldState = { const oldState = {
repo1: { "ns/one": [branch1]
branch1
}
}; };
const repo2 = { repo2: { branch3 } }; const newState = reducer(oldState, fetchBranchSuccess(repository, branch3));
const newState = reducer(oldState, fetchBranchSuccess(repo2, branch2)); expect(newState["ns/one"]).toEqual([branch1]);
expect(newState["repo1"]).toBe({ branch1 }); expect(newState["foo/bar"]).toEqual([branch3]);
expect(newState["repo2"]).toBe({ branch2, 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" version "0.0.2"
resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85" resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85"
"@scm-manager/ui-bundler@^0.0.26": "@scm-manager/ui-bundler@^0.0.27":
version "0.0.26" version "0.0.27"
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.26.tgz#4676a7079b781b33fa1989c6643205c3559b1f66" resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.27.tgz#3ed2c7826780b9a1a9ea90464332640cfb5d54b5"
integrity sha512-cBU1xq6gDy1Vw9AGOzsR763+JmBeraTaC/KQfxT3I6XyZJ2brIfG1m5QYcAcHWvDxq3mYMogpI5rfShw14L4/w==
dependencies: dependencies:
"@babel/core" "^7.0.0" "@babel/core" "^7.0.0"
"@babel/plugin-proposal-class-properties" "^7.0.0" "@babel/plugin-proposal-class-properties" "^7.0.0"