From 5687a552b8fc91fb69a8149ee33b580d0df3214e Mon Sep 17 00:00:00 2001 From: Philipp Czora Date: Fri, 14 Sep 2018 16:15:13 +0200 Subject: [PATCH] Added possibility to fetch changesets by branches --- .../src/changesets/components/Changesets.js | 18 +- scm-ui/src/changesets/modules/changesets.js | 58 +++- .../src/changesets/modules/changesets.test.js | 296 +++++++++++------- scm-ui/src/createReduxStore.js | 2 + scm-ui/src/repos/modules/branches.js | 41 +++ scm-ui/src/repos/modules/branches.test.js | 92 +++++- 6 files changed, 359 insertions(+), 148 deletions(-) diff --git a/scm-ui/src/changesets/components/Changesets.js b/scm-ui/src/changesets/components/Changesets.js index ea0ac7aad1..f9feb51829 100644 --- a/scm-ui/src/changesets/components/Changesets.js +++ b/scm-ui/src/changesets/components/Changesets.js @@ -3,8 +3,12 @@ import { connect } from "react-redux"; import ChangesetRow from "./ChangesetRow"; import type {Changeset} from "@scm-manager/ui-types"; -import { fetchChangesetsByNamespaceAndName, getChangesetsForNameAndNamespaceFromState } from "../modules/changesets"; +import { + fetchChangesetsByNamespaceAndName, + getChangesets, +} from "../modules/changesets"; import { translate } from "react-i18next"; +import {fetchBranchesByNamespaceAndName} from "../../repos/modules/branches"; type Props = { changesets: Changeset[], @@ -15,6 +19,7 @@ class Changesets extends React.Component { componentDidMount() { const {namespace, name} = this.props.repository; this.props.fetchChangesetsByNamespaceAndName(namespace, name); + this.props.fetchBranchesByNamespaceAndName(namespace, name); } render() { @@ -24,7 +29,7 @@ class Changesets extends React.Component { } return - Changesets + {changesets.map((changeset, index) => { @@ -36,15 +41,20 @@ class Changesets extends React.Component { } const mapStateToProps = (state, ownProps) => { + const {namespace, name} = ownProps.repository; return { - changesets: getChangesetsForNameAndNamespaceFromState(ownProps.repository.namespace, ownProps.repository.name, state) + changesets: getChangesets(namespace, name, "", state) } }; const mapDispatchToProps = dispatch => { return { fetchChangesetsByNamespaceAndName: (namespace: string, name: string) => { - dispatch(fetchChangesetsByNamespaceAndName(namespace, name)) + dispatch(fetchChangesetsByNamespaceAndName(namespace, name)); + }, + + fetchBranchesByNamespaceAndName: (namespace: string, name: string) => { + dispatch(fetchBranchesByNamespaceAndName(namespace, name)); } } }; diff --git a/scm-ui/src/changesets/modules/changesets.js b/scm-ui/src/changesets/modules/changesets.js index 1aaa77f961..d77a1423aa 100644 --- a/scm-ui/src/changesets/modules/changesets.js +++ b/scm-ui/src/changesets/modules/changesets.js @@ -26,39 +26,61 @@ export function fetchChangesetsByNamespaceAndName(namespace: string, name: strin } } -export function fetchChangesetsPending(namespace: string, name: string): Action { +export function fetchChangesetsByNamespaceNameAndBranch(namespace: string, name: string, branch: string) { + return function (dispatch: any) { + dispatch(fetchChangesetsPending(namespace, name, branch)); + return apiClient.get(REPO_URL + "/" + namespace + "/" + name + "/branches/" + branch + "/changesets").then(response => response.json()) + .then(data => { + dispatch(fetchChangesetsSuccess(data, namespace, name, branch)) + }).catch(cause => { + dispatch(fetchChangesetsFailure(namespace, name, branch, cause)) + }) + } +} + +export function fetchChangesetsPending(namespace: string, name: string, branch?: string): Action { return { type: FETCH_CHANGESETS_PENDING, payload: { namespace, - name + name, + branch }, - itemId: namespace + "/" + name + itemId: createItemId(namespace, name, branch) } } -export function fetchChangesetsSuccess(collection: any, namespace: string, name: string): Action { +export function fetchChangesetsSuccess(collection: any, namespace: string, name: string, branch?: string): Action { return { type: FETCH_CHANGESETS_SUCCESS, - payload: {collection, namespace, name}, - itemId: namespace + "/" + name + payload: {collection, namespace, name, branch}, + itemId: createItemId(namespace, name, branch) } } -function fetchChangesetsFailure(namespace: string, name: string, error: Error): Action { +function fetchChangesetsFailure(namespace: string, name: string, branch?: string, error: Error): Action { return { type: FETCH_CHANGESETS_FAILURE, payload: { namespace, name, + branch, error }, - itemId: namespace + "/" + name + itemId: createItemId(namespace, name, branch) } } +function createItemId(namespace: string, name: string, branch?: string): string { + let itemId = namespace + "/" + name; + if (branch && branch !== "") { + itemId = itemId + "/" + branch; + } + return itemId; +} + // reducer -export default function reducer(state: any = {}, action: any = {}) { +export default function reducer(state: any = {}, action: Action = {type: "UNKNOWN"}): Object { switch (action.type) { case FETCH_CHANGESETS_SUCCESS: const {namespace, name} = action.payload; @@ -90,7 +112,7 @@ function extractChangesetsByIds(data: any, oldChangesetsByIds: any) { } //selectors -export function getChangesetsForNameAndNamespaceFromState(namespace: string, name: string, state: Object) { +export function getChangesetsForNamespaceAndNameFromState(namespace: string, name: string, state: Object) { const key = namespace + "/" + name; if (!state.changesets[key]) { return null; @@ -98,11 +120,19 @@ export function getChangesetsForNameAndNamespaceFromState(namespace: string, nam return Object.values(state.changesets[key].byId); } -export function isFetchChangesetsPending( state: Object, namespace: string, name: string) { - return isPending(state, FETCH_CHANGESETS, namespace + "/" + name) +export function getChangesets(namespace: string, name: string, branch: string, state: Object) { + const key = createItemId(namespace, name, branch); + if (!state.changesets[key]) { + return null; + } + return Object.values(state.changesets[key].byId); } -export function getFetchChangesetsFailure( state: Object, namespace: string, name: string) { - return getFailure(state, FETCH_CHANGESETS, namespace + "/" + name); +export function isFetchChangesetsPending(state: Object, namespace: string, name: string, branch?: string) { + return isPending(state, FETCH_CHANGESETS, createItemId(namespace, name, branch)) +} + +export function getFetchChangesetsFailure(state: Object, namespace: string, name: string, branch?: string) { + return getFailure(state, FETCH_CHANGESETS, createItemId(namespace, name, branch)); } diff --git a/scm-ui/src/changesets/modules/changesets.test.js b/scm-ui/src/changesets/modules/changesets.test.js index c514978144..e206178538 100644 --- a/scm-ui/src/changesets/modules/changesets.test.js +++ b/scm-ui/src/changesets/modules/changesets.test.js @@ -4,95 +4,129 @@ import configureMockStore from "redux-mock-store"; import thunk from "redux-thunk"; import fetchMock from "fetch-mock"; import { - FETCH_CHANGESETS, FETCH_CHANGESETS_FAILURE, + FETCH_CHANGESETS, + FETCH_CHANGESETS_FAILURE, FETCH_CHANGESETS_PENDING, FETCH_CHANGESETS_SUCCESS, fetchChangesetsByNamespaceAndName, - fetchChangesetsSuccess, getChangesetsForNameAndNamespaceFromState, getFetchChangesetsFailure, isFetchChangesetsPending + fetchChangesetsByNamespaceNameAndBranch, + fetchChangesetsSuccess, + getChangesets, + getChangesetsForNamespaceAndNameFromState, + getFetchChangesetsFailure, + isFetchChangesetsPending } from "./changesets"; import reducer from "./changesets"; const collection = {}; -describe("fetching of changesets", () => { - const URL = "/api/rest/v2/repositories/foo/bar/changesets"; - const mockStore = configureMockStore([thunk]); +describe("changesets", () => { + describe("fetching of changesets", () => { + const DEFAULT_BRANCH_URL = "/api/rest/v2/repositories/foo/bar/changesets"; + const SPECIFIC_BRANCH_URL = "/api/rest/v2/repositories/foo/bar/branches/specific/changesets"; + const mockStore = configureMockStore([thunk]); - afterEach(() => { - fetchMock.reset(); - fetchMock.restore(); - }); - - it("should fetch changesets", () => { - fetchMock.getOnce(URL, "{}"); - - const expectedActions = [ - {type: FETCH_CHANGESETS_PENDING, payload: {namespace: "foo", name: "bar"}, - itemId: "foo/bar"}, - { - type: FETCH_CHANGESETS_SUCCESS, - payload: {collection, namespace: "foo", name: "bar"}, - itemId: "foo/bar" - } - ]; - - const store = mockStore({}); - return store.dispatch(fetchChangesetsByNamespaceAndName("foo", "bar")).then(() => { - expect(store.getActions()).toEqual(expectedActions); + afterEach(() => { + fetchMock.reset(); + fetchMock.restore(); }); - }); - it("should fail fetching changesets on error", () => { - fetchMock.getOnce(URL, 500); + it("should fetch changesets for default branch", () => { + fetchMock.getOnce(DEFAULT_BRANCH_URL, "{}"); - const expectedActions = [ - {type: FETCH_CHANGESETS_PENDING, payload: {namespace: "foo", name: "bar"}, - itemId: "foo/bar"}, - { - type: FETCH_CHANGESETS_SUCCESS, - payload: {collection, namespace: "foo", name: "bar"}, - itemId: "foo/bar" - } - ]; + const expectedActions = [ + { + type: FETCH_CHANGESETS_PENDING, payload: {namespace: "foo", name: "bar"}, + itemId: "foo/bar" + }, + { + type: FETCH_CHANGESETS_SUCCESS, + payload: {collection, namespace: "foo", name: "bar"}, + itemId: "foo/bar" + } + ]; - const store = mockStore({}); - return store.dispatch(fetchChangesetsByNamespaceAndName("foo", "bar")).then(() => { - expect(store.getActions()[0]).toEqual(expectedActions[0]); - expect(store.getActions()[1].type).toEqual(FETCH_CHANGESETS_FAILURE); - expect(store.getActions()[1].payload).toBeDefined(); + const store = mockStore({}); + return store.dispatch(fetchChangesetsByNamespaceAndName("foo", "bar")).then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); }); - }) -}); -describe("changesets reducer", () => { - const responseBody = { - _embedded: { - changesets: [ - {id: "changeset1", author: {mail: "z@phod.com", name: "zaphod"}}, - {id: "changeset2", description: "foo"}, - {id: "changeset3", description: "bar"}, - ], - _embedded: { - tags: [], - branches: [], - parents: [] - } - } - }; + it("should fetch changesets for specific branch", () => { + fetchMock.getOnce(SPECIFIC_BRANCH_URL, "{}"); - it("should set state to received changesets", () => { - const newState = reducer({}, fetchChangesetsSuccess(responseBody, "foo", "bar")); - expect(newState).toBeDefined(); - expect(newState["foo/bar"].byId["changeset1"].author.mail).toEqual("z@phod.com"); - expect(newState["foo/bar"].byId["changeset2"].description).toEqual("foo"); - expect(newState["foo/bar"].byId["changeset3"].description).toEqual("bar"); + const expectedActions = [ + { + type: FETCH_CHANGESETS_PENDING, payload: {namespace: "foo", name: "bar", branch: "specific"}, + itemId: "foo/bar/specific" + }, + { + type: FETCH_CHANGESETS_SUCCESS, + payload: {collection, namespace: "foo", name: "bar", branch: "specific"}, + itemId: "foo/bar/specific" + } + ]; + + const store = mockStore({}); + return store.dispatch(fetchChangesetsByNamespaceNameAndBranch("foo", "bar", "specific")).then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + }); + + it("should fail fetching changesets on error", () => { + fetchMock.getOnce(DEFAULT_BRANCH_URL, 500); + + const expectedActions = [ + { + type: FETCH_CHANGESETS_PENDING, payload: {namespace: "foo", name: "bar"}, + itemId: "foo/bar" + }, + { + type: FETCH_CHANGESETS_SUCCESS, + payload: {collection, namespace: "foo", name: "bar"}, + itemId: "foo/bar" + } + ]; + + const store = mockStore({}); + return store.dispatch(fetchChangesetsByNamespaceAndName("foo", "bar")).then(() => { + expect(store.getActions()[0]).toEqual(expectedActions[0]); + expect(store.getActions()[1].type).toEqual(FETCH_CHANGESETS_FAILURE); + expect(store.getActions()[1].payload).toBeDefined(); + }); + }) + + it("should fail fetching changesets for specific branch on error", () => { + fetchMock.getOnce(SPECIFIC_BRANCH_URL, 500); + + const expectedActions = [ + { + type: FETCH_CHANGESETS_PENDING, payload: {namespace: "foo", name: "bar", branch: "specific"}, + itemId: "foo/bar/specific" + }, + { + type: FETCH_CHANGESETS_SUCCESS, + payload: {collection, namespace: "foo", name: "bar", branch: "specific"}, + itemId: "foo/bar/specific" + } + ]; + + const store = mockStore({}); + return store.dispatch(fetchChangesetsByNamespaceNameAndBranch("foo", "bar", "specific")).then(() => { + expect(store.getActions()[0]).toEqual(expectedActions[0]); + expect(store.getActions()[1].type).toEqual(FETCH_CHANGESETS_FAILURE); + expect(store.getActions()[1].payload).toBeDefined(); + }); + }) }); - it("should not delete existing changesets from state", () => { + describe("changesets reducer", () => { const responseBody = { _embedded: { changesets: [ {id: "changeset1", author: {mail: "z@phod.com", name: "zaphod"}}, + {id: "changeset2", description: "foo"}, + {id: "changeset3", description: "bar"}, ], _embedded: { tags: [], @@ -101,65 +135,89 @@ describe("changesets reducer", () => { } } }; - const newState = reducer({ - "foo/bar": { - byId: { - ["changeset2"]: { - id: "changeset2", - author: {mail: "mail@author.com", name: "author"} + + it("should set state to received changesets", () => { + const newState = reducer({}, fetchChangesetsSuccess(responseBody, "foo", "bar")); + expect(newState).toBeDefined(); + expect(newState["foo/bar"].byId["changeset1"].author.mail).toEqual("z@phod.com"); + 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: [] } } - } - }, fetchChangesetsSuccess(responseBody, "foo", "bar")); - expect(newState["foo/bar"].byId["changeset2"]).toBeDefined(); - expect(newState["foo/bar"].byId["changeset1"]).toBeDefined(); - }) -}); - -describe("changeset selectors", () => { - const error = new Error("Something went wrong"); - - it("should get all changesets for a given namespace and name", () => { - const state = { - changesets: { - ["foo/bar"]: { + }; + const newState = reducer({ + "foo/bar": { byId: { - "id1": {id: "id1"}, - "id2": {id: "id2"} + ["changeset2"]: { + id: "changeset2", + author: {mail: "mail@author.com", name: "author"} + } } } - } - }; - const result = getChangesetsForNameAndNamespaceFromState("foo", "bar", state); - expect(result).toContainEqual({id: "id1"}) + }, fetchChangesetsSuccess(responseBody, "foo", "bar")); + expect(newState["foo/bar"].byId["changeset2"]).toBeDefined(); + expect(newState["foo/bar"].byId["changeset1"]).toBeDefined(); + }) }); - it("should return true, when fetching changesets is pending", () => { - const state = { - pending: { - [FETCH_CHANGESETS + "/foo/bar"]: true - } - }; + describe("changeset selectors", () => { + const error = new Error("Something went wrong"); + + 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 = getChangesetsForNamespaceAndNameFromState("foo", "bar", state); + const result = getChangesets("foo", "bar", "", state); + expect(result).toContainEqual({id: "id1"}) + }); + + it("should return true, when fetching changesets is pending", () => { + const state = { + pending: { + [FETCH_CHANGESETS + "/foo/bar"]: true + } + }; + + expect(isFetchChangesetsPending(state, "foo", "bar")).toBeTruthy(); + }); + + it("should return false, when fetching changesets is not pending", () => { + expect(isFetchChangesetsPending({}, "foo", "bar")).toEqual(false); + }); + + it("should return error if fetching changesets failed", () => { + const state = { + failure: { + [FETCH_CHANGESETS + "/foo/bar"]: error + } + }; + + expect(getFetchChangesetsFailure(state, "foo", "bar")).toEqual(error); + }); + + it("should return false if fetching changesets did not fail", () => { + expect(getFetchChangesetsFailure({}, "foo", "bar")).toBeUndefined(); + }) - expect(isFetchChangesetsPending(state, "foo", "bar")).toBeTruthy(); }); - - it("should return false, when fetching changesets is not pending", () => { - expect(isFetchChangesetsPending({}, "foo", "bar")).toEqual(false); - }); - - it("should return error if fetching changesets failed", () => { - const state = { - failure: { - [FETCH_CHANGESETS + "/foo/bar"]: error - } - }; - - expect(getFetchChangesetsFailure(state, "foo", "bar")).toEqual(error); - }); - - it("should return false if fetching changesets did not fail", () => { - expect(getFetchChangesetsFailure({}, "foo", "bar")).toBeUndefined(); - }) - }); diff --git a/scm-ui/src/createReduxStore.js b/scm-ui/src/createReduxStore.js index a63a6edcdb..5c4fb20410 100644 --- a/scm-ui/src/createReduxStore.js +++ b/scm-ui/src/createReduxStore.js @@ -15,6 +15,7 @@ import failure from "./modules/failure"; import config from "./config/modules/config"; import type { BrowserHistory } from "history/createBrowserHistory"; +import branches from "./repos/modules/branches"; function createReduxStore(history: BrowserHistory) { const composeEnhancers = @@ -28,6 +29,7 @@ function createReduxStore(history: BrowserHistory) { repos, repositoryTypes, changesets, + branches, groups, auth, config diff --git a/scm-ui/src/repos/modules/branches.js b/scm-ui/src/repos/modules/branches.js index dde5cb379b..4f1624184f 100644 --- a/scm-ui/src/repos/modules/branches.js +++ b/scm-ui/src/repos/modules/branches.js @@ -50,4 +50,45 @@ export function fetchBranchesFailure(namespace: string, name: string, error: Err // Reducers +export default function reducer(state: Object = {}, action: Action = {type: "UNKNOWN"}): Object { + switch (action.type) { + case FETCH_BRANCHES_SUCCESS: + const key = action.payload.namespace + "/" + action.payload.name; + let oldBranchesByNames = {[key]: {}}; + if (state[key] !== undefined) { + oldBranchesByNames[key] = state[key] + } + return { + [key]: { + byNames: extractBranchesByNames(action.payload.data, oldBranchesByNames[key].byNames) + } + }; + default: + return state; + } + +} + +function extractBranchesByNames(data: any, oldBranchesByNames: any): Branch[] { + const branches = data._embedded.branches; + const branchesByNames = {}; + + for (let branch of branches) { + branchesByNames[branch.name] = branch; + } + + for (let name in oldBranchesByNames) { + branchesByNames[name] = oldBranchesByNames[name] + } + return branchesByNames; +} + // Selectors + +export function getBranchesForNamespaceAndNameFromState(namespace: string, name: string, state: Object) { + const key = namespace + "/" + name; + if (!state.branches[key]) { + return null; + } + return Object.values(state.branches[key].byNames); +} diff --git a/scm-ui/src/repos/modules/branches.test.js b/scm-ui/src/repos/modules/branches.test.js index 67c5950c5a..83a6f73b03 100644 --- a/scm-ui/src/repos/modules/branches.test.js +++ b/scm-ui/src/repos/modules/branches.test.js @@ -5,13 +5,25 @@ import { FETCH_BRANCHES_FAILURE, FETCH_BRANCHES_PENDING, FETCH_BRANCHES_SUCCESS, - fetchBranchesByNamespaceAndName + fetchBranchesByNamespaceAndName, getBranchesForNamespaceAndNameFromState } from "./branches"; +import reducer from "./branches"; + + +const namespace = "foo"; +const name = "bar"; +const key = namespace + "/" + name; + +const branch1 = {name: "branch1", revision: "revision1"}; +const branch2 = {name: "branch2", revision: "revision2"}; +const branch3 = {name: "branch3", revision: "revision3"}; + describe("fetch branches", () => { const URL = "/api/rest/v2/repositories/foo/bar/branches"; const mockStore = configureMockStore([thunk]); + afterEach(() => { fetchMock.reset(); fetchMock.restore(); @@ -24,17 +36,19 @@ describe("fetch branches", () => { fetchMock.getOnce(URL, "{}"); const expectedActions = [ - {type: FETCH_BRANCHES_PENDING, payload: {namespace: "foo", name: "bar"}, - itemId: "foo/bar"}, + { + type: FETCH_BRANCHES_PENDING, payload: {namespace, name}, + itemId: key + }, { type: FETCH_BRANCHES_SUCCESS, - payload: {data: collection, namespace: "foo", name: "bar"}, - itemId: "foo/bar" + payload: {data: collection, namespace, name}, + itemId: key } ]; const store = mockStore({}); - return store.dispatch(fetchBranchesByNamespaceAndName("foo", "bar")).then(() => { + return store.dispatch(fetchBranchesByNamespaceAndName(namespace, name)).then(() => { expect(store.getActions()).toEqual(expectedActions); }); }); @@ -45,19 +59,75 @@ describe("fetch branches", () => { fetchMock.getOnce(URL, 500); const expectedActions = [ - {type: FETCH_BRANCHES_PENDING, payload: {namespace: "foo", name: "bar"}, - itemId: "foo/bar"}, + { + type: FETCH_BRANCHES_PENDING, payload: {namespace, name}, + itemId: key + }, { type: FETCH_BRANCHES_FAILURE, - payload: {error: collection, namespace: "foo", name: "bar"}, - itemId: "foo/bar" + payload: {error: collection, namespace, name}, + itemId: key } ]; const store = mockStore({}); - return store.dispatch(fetchBranchesByNamespaceAndName("foo", "bar")).then(() => { + return store.dispatch(fetchBranchesByNamespaceAndName(namespace, name)).then(() => { expect(store.getActions()[0]).toEqual(expectedActions[0]); expect(store.getActions()[1].type).toEqual(FETCH_BRANCHES_FAILURE); }); }) }); + +describe("branches reducer", () => { + + const branches = { + _embedded: { + branches: [branch1, branch2] + } + }; + const action = { + type: FETCH_BRANCHES_SUCCESS, + payload: { + namespace, + name, + data: branches + } + }; + + it("should update state according to successful fetch", () => { + const newState = reducer({}, action); + expect(newState).toBeDefined(); + expect(newState[key]).toBeDefined(); + expect(newState[key].byNames["branch1"]).toEqual(branch1); + expect(newState[key].byNames["branch2"]).toEqual(branch2); + }); + + it("should not delete existing branches from state", () => { + const oldState = { + "foo/bar": { byNames: { + "branch3": branch3 + }} + }; + + const newState = reducer(oldState, action); + console.log(newState); + expect(newState[key].byNames["branch1"]).toEqual(branch1); + expect(newState[key].byNames["branch2"]).toEqual(branch2); + expect(newState[key].byNames["branch3"]).toEqual(branch3); + }); +}); + +describe("branch selectors", () => { + it("should get branches for namespace and name", () => { + const state = { + branches: { + [key]: { + byNames: { + "branch1": branch1 + } + } + } + }; + getBranchesForNamespaceAndNameFromState(namespace, name, state); + }) +});
Changesets