diff --git a/scm-ui-components/packages/ui-types/src/Branches.js b/scm-ui-components/packages/ui-types/src/Branches.js index f209de34a0..2dee365424 100644 --- a/scm-ui-components/packages/ui-types/src/Branches.js +++ b/scm-ui-components/packages/ui-types/src/Branches.js @@ -4,5 +4,6 @@ import type {Links} from "./hal"; export type Branch = { name: string, revision: string, + defaultBranch?: boolean, _links: Links } diff --git a/scm-ui/package.json b/scm-ui/package.json index 6844ea3ec7..78c7b0e684 100644 --- a/scm-ui/package.json +++ b/scm-ui/package.json @@ -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", diff --git a/scm-ui/src/repos/branches/components/BranchRow.js b/scm-ui/src/repos/branches/components/BranchRow.js index 06cb52c268..d8f70575b3 100644 --- a/scm-ui/src/repos/branches/components/BranchRow.js +++ b/scm-ui/src/repos/branches/components/BranchRow.js @@ -19,7 +19,7 @@ const styles = { }; class BranchRow extends React.Component { - 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 { render() { const { baseUrl, branch } = this.props; const to = `${baseUrl}/${encodeURIComponent(branch.name)}/info`; - return ( - - {this.renderLink(to, branch.name, branch.defaultBranch)} - - ); + return ( + + {this.renderLink(to, branch.name, branch.defaultBranch)} + + ); } } diff --git a/scm-ui/src/repos/branches/containers/BranchView.js b/scm-ui/src/repos/branches/containers/BranchView.js index 5a90812ac9..959c4519d0 100644 --- a/scm-ui/src/repos/branches/containers/BranchView.js +++ b/scm-ui/src/repos/branches/containers/BranchView.js @@ -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 { 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 { 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)); } }; }; diff --git a/scm-ui/src/repos/branches/modules/branches.js b/scm-ui/src/repos/branches/modules/branches.js index a080a3877f..dd542d0183 100644 --- a/scm-ui/src/repos/branches/modules/branches.js +++ b/scm-ui/src/repos/branches/modules/branches.js @@ -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; } diff --git a/scm-ui/src/repos/branches/modules/branches.test.js b/scm-ui/src/repos/branches/modules/branches.test.js index cc67aaf33a..30b81975d6 100644 --- a/scm-ui/src/repos/branches/modules/branches.test.js +++ b/scm-ui/src/repos/branches/modules/branches.test.js @@ -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); }); }); diff --git a/scm-ui/yarn.lock b/scm-ui/yarn.lock index 5b6ec89ef3..4440b28f6c 100644 --- a/scm-ui/yarn.lock +++ b/scm-ui/yarn.lock @@ -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"