From 9a4896b55dda99033f73a8fbfb45a3524d34e6d5 Mon Sep 17 00:00:00 2001 From: Philipp Czora Date: Mon, 6 Aug 2018 15:41:20 +0200 Subject: [PATCH] Implemented editing of repos (in UI) --- scm-ui/public/locales/en/repos.json | 9 +- scm-ui/src/components/forms/Textarea.js | 5 +- scm-ui/src/containers/Main.js | 1 - scm-ui/src/repos/components/EditNavLink.js | 22 ++++ .../src/repos/components/EditNavLink.test.js | 32 +++++ scm-ui/src/repos/containers/Edit.js | 80 +++++++++++++ scm-ui/src/repos/containers/RepositoryRoot.js | 7 ++ scm-ui/src/repos/modules/repos.js | 71 +++++++++++ scm-ui/src/repos/modules/repos.test.js | 110 +++++++++++++++++- 9 files changed, 331 insertions(+), 6 deletions(-) create mode 100644 scm-ui/src/repos/components/EditNavLink.js create mode 100644 scm-ui/src/repos/components/EditNavLink.test.js create mode 100644 scm-ui/src/repos/containers/Edit.js diff --git a/scm-ui/public/locales/en/repos.json b/scm-ui/public/locales/en/repos.json index e28ea3754c..29074a49ba 100644 --- a/scm-ui/public/locales/en/repos.json +++ b/scm-ui/public/locales/en/repos.json @@ -26,8 +26,15 @@ "title": "Create Repository", "subtitle": "Create a new repository" }, + "edit": { + "title": "Edit Repository", + "subtitle": "Edit an existing repository" + }, "repository-form": { - "submit": "Speichern" + "submit": "Save" + }, + "edit-nav-link": { + "label": "Edit" }, "delete-nav-action": { "label": "Delete", diff --git a/scm-ui/src/components/forms/Textarea.js b/scm-ui/src/components/forms/Textarea.js index 3144c00d0c..721bbae77f 100644 --- a/scm-ui/src/components/forms/Textarea.js +++ b/scm-ui/src/components/forms/Textarea.js @@ -42,9 +42,8 @@ class Textarea extends React.Component { }} placeholder={placeholder} onChange={this.handleInput} - > - {value} - + value={value} + /> ); diff --git a/scm-ui/src/containers/Main.js b/scm-ui/src/containers/Main.js index 2c93cb06d0..331d5e7c7b 100644 --- a/scm-ui/src/containers/Main.js +++ b/scm-ui/src/containers/Main.js @@ -47,7 +47,6 @@ class Main extends React.Component { authenticated={authenticated} /> string, repository: Repository }; + +class EditNavLink extends React.Component { + isEditable = () => { + return this.props.repository._links.update; + }; + render() { + if (!this.isEditable()) { + return null; + } + const { editUrl, t } = this.props; + return ; + } +} + +export default translate("repos")(EditNavLink); diff --git a/scm-ui/src/repos/components/EditNavLink.test.js b/scm-ui/src/repos/components/EditNavLink.test.js new file mode 100644 index 0000000000..8289f0fb3b --- /dev/null +++ b/scm-ui/src/repos/components/EditNavLink.test.js @@ -0,0 +1,32 @@ +import React from "react"; +import { mount, shallow } from "enzyme"; +import "../../tests/enzyme"; +import "../../tests/i18n"; +import EditNavLink from "./EditNavLink"; + +jest.mock("../../components/modals/ConfirmAlert"); +jest.mock("../../components/navigation/NavLink", () => () =>
foo
); + +describe("EditNavLink", () => { + it("should render nothing, if the modify link is missing", () => { + const repository = { + _links: {} + }; + + const navLink = shallow(); + expect(navLink.text()).toBe(""); + }); + + it("should render the navLink", () => { + const repository = { + _links: { + update: { + href: "/repositories" + } + } + }; + + const navLink = mount(); + expect(navLink.text()).toBe("foo"); + }); +}); diff --git a/scm-ui/src/repos/containers/Edit.js b/scm-ui/src/repos/containers/Edit.js new file mode 100644 index 0000000000..3a5662ecf1 --- /dev/null +++ b/scm-ui/src/repos/containers/Edit.js @@ -0,0 +1,80 @@ +// @flow +import React from "react"; +import { connect } from "react-redux"; +import { translate } from "react-i18next"; +import { Page } from "../../components/layout"; +import RepositoryForm from "../components/form"; +import type { Repository } from "../types/Repositories"; +import { + modifyRepo, + isModifyRepoPending, + getModifyRepoFailure +} from "../modules/repos"; +import { withRouter } from "react-router-dom"; +import type { History } from "history"; + +type Props = { + repository: Repository, + modifyRepo: (Repository, () => void) => void, + modifyLoading: boolean, + error: Error, + + // context props + t: string => string, + history: History +}; + +class Edit extends React.Component { + componentDidMount() {} + + repoModified = () => { + const { history, repository } = this.props; + history.push(`/repo/${repository.namespace}/${repository.name}`); + }; + + render() { + const { t, modifyLoading, error } = this.props; + return ( + + { + this.props.modifyRepo(repo, this.repoModified); + }} + /> + + ); + } +} + +const mapStateToProps = (state, ownProps) => { + const { namespace, name } = ownProps.repository; + + const modifyLoading = isModifyRepoPending(state, namespace, name); + + const error = getModifyRepoFailure(state, namespace, name); + + return { + modifyLoading, + error + }; +}; + +const mapDispatchToProps = dispatch => { + return { + modifyRepo: (repo: Repository, callback: () => void) => { + dispatch(modifyRepo(repo, callback)); + } + }; +}; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(translate("repos")(withRouter(Edit))); diff --git a/scm-ui/src/repos/containers/RepositoryRoot.js b/scm-ui/src/repos/containers/RepositoryRoot.js index 97390aaa52..fe343b4be3 100644 --- a/scm-ui/src/repos/containers/RepositoryRoot.js +++ b/scm-ui/src/repos/containers/RepositoryRoot.js @@ -17,8 +17,10 @@ import { translate } from "react-i18next"; import { Navigation, NavLink, Section } from "../../components/navigation"; import RepositoryDetails from "../components/RepositoryDetails"; import DeleteNavAction from "../components/DeleteNavAction"; +import Edit from "../containers/Edit"; import type { History } from "history"; +import EditNavLink from "../components/EditNavLink"; type Props = { namespace: string, @@ -91,10 +93,15 @@ class RepositoryRoot extends React.Component { exact component={() => } /> + } + />
+
diff --git a/scm-ui/src/repos/modules/repos.js b/scm-ui/src/repos/modules/repos.js index 8f401e00ce..da0a69850e 100644 --- a/scm-ui/src/repos/modules/repos.js +++ b/scm-ui/src/repos/modules/repos.js @@ -22,6 +22,11 @@ export const CREATE_REPO_SUCCESS = `${CREATE_REPO}_${types.SUCCESS_SUFFIX}`; export const CREATE_REPO_FAILURE = `${CREATE_REPO}_${types.FAILURE_SUFFIX}`; export const CREATE_REPO_RESET = `${CREATE_REPO}_${types.RESET_SUFFIX}`; +export const MODIFY_REPO = "scm/repos/MODIFY_REPO"; +export const MODIFY_REPO_PENDING = `${MODIFY_REPO}_${types.PENDING_SUFFIX}`; +export const MODIFY_REPO_SUCCESS = `${MODIFY_REPO}_${types.SUCCESS_SUFFIX}`; +export const MODIFY_REPO_FAILURE = `${MODIFY_REPO}_${types.FAILURE_SUFFIX}`; + export const DELETE_REPO = "scm/repos/DELETE_REPO"; export const DELETE_REPO_PENDING = `${DELETE_REPO}_${types.PENDING_SUFFIX}`; export const DELETE_REPO_SUCCESS = `${DELETE_REPO}_${types.SUCCESS_SUFFIX}`; @@ -188,6 +193,54 @@ export function createRepoReset(): Action { }; } +// modify + +export function modifyRepo(repository: Repository, callback?: () => void) { + return function(dispatch: any) { + dispatch(modifyRepoPending(repository)); + + return apiClient + .put(repository._links.update.href, repository, CONTENT_TYPE) + .then(() => { + dispatch(modifyRepoSuccess(repository)); + if (callback) { + callback(); + } + }) + .catch(cause => { + const error = new Error(`failed to modify repo: ${cause.message}`); + dispatch(modifyRepoFailure(repository, error)); + }); + }; +} + +export function modifyRepoPending(repository: Repository): Action { + return { + type: MODIFY_REPO_PENDING, + payload: repository, + itemId: createIdentifier(repository) + }; +} + +export function modifyRepoSuccess(repository: Repository): Action { + return { + type: MODIFY_REPO_SUCCESS, + payload: repository, + itemId: createIdentifier(repository) + }; +} + +export function modifyRepoFailure( + repository: Repository, + error: Error +): Action { + return { + type: MODIFY_REPO_FAILURE, + payload: { error, repository }, + itemId: createIdentifier(repository) + }; +} + // delete export function deleteRepo(repository: Repository, callback?: () => void) { @@ -288,6 +341,8 @@ export default function reducer( switch (action.type) { case FETCH_REPOS_SUCCESS: return normalizeByNamespaceAndName(action.payload); + case MODIFY_REPO_SUCCESS: + return reducerByNames(state, action.payload); case FETCH_REPO_SUCCESS: return reducerByNames(state, action.payload); default: @@ -359,6 +414,22 @@ export function getCreateRepoFailure(state: Object) { return getFailure(state, CREATE_REPO); } +export function isModifyRepoPending( + state: Object, + namespace: string, + name: string +) { + return isPending(state, MODIFY_REPO, namespace + "/" + name); +} + +export function getModifyRepoFailure( + state: Object, + namespace: string, + name: string +) { + return getFailure(state, MODIFY_REPO, namespace + "/" + name); +} + export function isDeleteRepoPending( state: Object, namespace: string, diff --git a/scm-ui/src/repos/modules/repos.test.js b/scm-ui/src/repos/modules/repos.test.js index 0544f56d16..27dfb481a0 100644 --- a/scm-ui/src/repos/modules/repos.test.js +++ b/scm-ui/src/repos/modules/repos.test.js @@ -37,7 +37,15 @@ import reducer, { DELETE_REPO_PENDING, DELETE_REPO_FAILURE, isDeleteRepoPending, - getDeleteRepoFailure + getDeleteRepoFailure, + modifyRepo, + MODIFY_REPO_PENDING, + MODIFY_REPO_SUCCESS, + MODIFY_REPO_FAILURE, + MODIFY_REPO, + isModifyRepoPending, + getModifyRepoFailure, + modifyRepoSuccess } from "./repos"; import type { Repository, RepositoryCollection } from "../types/Repositories"; @@ -485,6 +493,64 @@ describe("repos fetch", () => { expect(actions[1].payload.error).toBeDefined(); }); }); + + it("should successfully modify slarti/fjords repo", () => { + fetchMock.putOnce(slartiFjords._links.update.href, { + status: 204 + }); + + let editedFjords = {...slartiFjords}; + editedFjords.description = "coast of africa"; + + const store = mockStore({}); + + return store.dispatch(modifyRepo(editedFjords)).then(() => { + const actions = store.getActions(); + expect(actions[0].type).toEqual(MODIFY_REPO_PENDING); + expect(actions[1].type).toEqual(MODIFY_REPO_SUCCESS); + }); + }); + + it("should successfully modify slarti/fjords repo and call the callback", () => { + fetchMock.putOnce(slartiFjords._links.update.href, { + status: 204 + }); + + let editedFjords = {...slartiFjords}; + editedFjords.description = "coast of africa"; + + const store = mockStore({}); + + let called = false; + const callback = () => { + called = true; + }; + + return store.dispatch(modifyRepo(editedFjords, callback)).then(() => { + const actions = store.getActions(); + expect(actions[0].type).toEqual(MODIFY_REPO_PENDING); + expect(actions[1].type).toEqual(MODIFY_REPO_SUCCESS); + expect(called).toBe(true); + }); + }); + + it("should fail modifying on HTTP 500", () => { + fetchMock.putOnce(slartiFjords._links.update.href, { + status: 500 + }); + + let editedFjords = { ...slartiFjords }; + editedFjords.description = "coast of africa"; + + const store = mockStore({}); + + return store.dispatch(modifyRepo(editedFjords)).then(() => { + const actions = store.getActions(); + expect(actions[0].type).toEqual(MODIFY_REPO_PENDING); + expect(actions[1].type).toEqual(MODIFY_REPO_FAILURE); + expect(actions[1].payload).toBeDefined(); + }); + }); }); describe("repos reducer", () => { @@ -521,6 +587,18 @@ describe("repos reducer", () => { const newState = reducer({}, fetchRepoSuccess(slartiFjords)); expect(newState.byNames["slarti/fjords"]).toBe(slartiFjords); }); + + it("should update reposByNames", () => { + const oldState = { + byNames: { + "slarti/fjords": slartiFjords + } + }; + let slartiFjordsEdited = { ...slartiFjords }; + slartiFjordsEdited.description = "I bless the rains down in Africa"; + const newState = reducer(oldState, modifyRepoSuccess(slartiFjordsEdited)); + expect(newState.byNames["slarti/fjords"]).toEqual(slartiFjordsEdited); + }); }); describe("repos selectors", () => { @@ -635,6 +713,36 @@ describe("repos selectors", () => { expect(getCreateRepoFailure({})).toBe(undefined); }); + // modify + + it("should return true, when modify repo is pending", () => { + const state = { + pending: { + [MODIFY_REPO + "/slarti/fjords"]: true + } + }; + + expect(isModifyRepoPending(state, "slarti", "fjords")).toEqual(true); + }); + + it("should return false, when modify repo is not pending", () => { + expect(isModifyRepoPending({}, "slarti", "fjords")).toEqual(false); + }); + + it("should return error, when modify repo failed", () => { + const state = { + failure: { + [MODIFY_REPO + "/slarti/fjords"]: error + } + }; + + expect(getModifyRepoFailure(state, "slarti", "fjords")).toEqual(error); + }); + + it("should return undefined, when modify did not fail", () => { + expect(getModifyRepoFailure({}, "slarti", "fjords")).toBeUndefined(); + }); + // delete it("should return true, when delete repo is pending", () => {