mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-05 04:55:50 +01:00
Implemented editing of repos (in UI)
This commit is contained in:
@@ -26,8 +26,15 @@
|
|||||||
"title": "Create Repository",
|
"title": "Create Repository",
|
||||||
"subtitle": "Create a new repository"
|
"subtitle": "Create a new repository"
|
||||||
},
|
},
|
||||||
|
"edit": {
|
||||||
|
"title": "Edit Repository",
|
||||||
|
"subtitle": "Edit an existing repository"
|
||||||
|
},
|
||||||
"repository-form": {
|
"repository-form": {
|
||||||
"submit": "Speichern"
|
"submit": "Save"
|
||||||
|
},
|
||||||
|
"edit-nav-link": {
|
||||||
|
"label": "Edit"
|
||||||
},
|
},
|
||||||
"delete-nav-action": {
|
"delete-nav-action": {
|
||||||
"label": "Delete",
|
"label": "Delete",
|
||||||
|
|||||||
@@ -42,9 +42,8 @@ class Textarea extends React.Component<Props> {
|
|||||||
}}
|
}}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
onChange={this.handleInput}
|
onChange={this.handleInput}
|
||||||
>
|
value={value}
|
||||||
{value}
|
/>
|
||||||
</textarea>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ class Main extends React.Component<Props> {
|
|||||||
authenticated={authenticated}
|
authenticated={authenticated}
|
||||||
/>
|
/>
|
||||||
<ProtectedRoute
|
<ProtectedRoute
|
||||||
exact
|
|
||||||
path="/repo/:namespace/:name"
|
path="/repo/:namespace/:name"
|
||||||
component={RepositoryRoot}
|
component={RepositoryRoot}
|
||||||
authenticated={authenticated}
|
authenticated={authenticated}
|
||||||
|
|||||||
22
scm-ui/src/repos/components/EditNavLink.js
Normal file
22
scm-ui/src/repos/components/EditNavLink.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
//@flow
|
||||||
|
import React from "react";
|
||||||
|
import { NavLink } from "../../components/navigation";
|
||||||
|
import { translate } from "react-i18next";
|
||||||
|
import type { Repository } from "../types/Repositories";
|
||||||
|
|
||||||
|
type Props = { editUrl: string, t: string => string, repository: Repository };
|
||||||
|
|
||||||
|
class EditNavLink extends React.Component<Props> {
|
||||||
|
isEditable = () => {
|
||||||
|
return this.props.repository._links.update;
|
||||||
|
};
|
||||||
|
render() {
|
||||||
|
if (!this.isEditable()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const { editUrl, t } = this.props;
|
||||||
|
return <NavLink to={editUrl} label={t("edit-nav-link.label")} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate("repos")(EditNavLink);
|
||||||
32
scm-ui/src/repos/components/EditNavLink.test.js
Normal file
32
scm-ui/src/repos/components/EditNavLink.test.js
Normal file
@@ -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", () => () => <div>foo</div>);
|
||||||
|
|
||||||
|
describe("EditNavLink", () => {
|
||||||
|
it("should render nothing, if the modify link is missing", () => {
|
||||||
|
const repository = {
|
||||||
|
_links: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
const navLink = shallow(<EditNavLink repository={repository} editUrl="" />);
|
||||||
|
expect(navLink.text()).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render the navLink", () => {
|
||||||
|
const repository = {
|
||||||
|
_links: {
|
||||||
|
update: {
|
||||||
|
href: "/repositories"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const navLink = mount(<EditNavLink repository={repository} editUrl="" />);
|
||||||
|
expect(navLink.text()).toBe("foo");
|
||||||
|
});
|
||||||
|
});
|
||||||
80
scm-ui/src/repos/containers/Edit.js
Normal file
80
scm-ui/src/repos/containers/Edit.js
Normal file
@@ -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<Props> {
|
||||||
|
componentDidMount() {}
|
||||||
|
|
||||||
|
repoModified = () => {
|
||||||
|
const { history, repository } = this.props;
|
||||||
|
history.push(`/repo/${repository.namespace}/${repository.name}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { t, modifyLoading, error } = this.props;
|
||||||
|
return (
|
||||||
|
<Page
|
||||||
|
title={t("edit.title")}
|
||||||
|
subtitle={t("edit.subtitle")}
|
||||||
|
error={error}
|
||||||
|
showContentOnError={true}
|
||||||
|
>
|
||||||
|
<RepositoryForm
|
||||||
|
repository={this.props.repository}
|
||||||
|
loading={modifyLoading}
|
||||||
|
submitForm={repo => {
|
||||||
|
this.props.modifyRepo(repo, this.repoModified);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)));
|
||||||
@@ -17,8 +17,10 @@ import { translate } from "react-i18next";
|
|||||||
import { Navigation, NavLink, Section } from "../../components/navigation";
|
import { Navigation, NavLink, Section } from "../../components/navigation";
|
||||||
import RepositoryDetails from "../components/RepositoryDetails";
|
import RepositoryDetails from "../components/RepositoryDetails";
|
||||||
import DeleteNavAction from "../components/DeleteNavAction";
|
import DeleteNavAction from "../components/DeleteNavAction";
|
||||||
|
import Edit from "../containers/Edit";
|
||||||
|
|
||||||
import type { History } from "history";
|
import type { History } from "history";
|
||||||
|
import EditNavLink from "../components/EditNavLink";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
namespace: string,
|
namespace: string,
|
||||||
@@ -91,10 +93,15 @@ class RepositoryRoot extends React.Component<Props> {
|
|||||||
exact
|
exact
|
||||||
component={() => <RepositoryDetails repository={repository} />}
|
component={() => <RepositoryDetails repository={repository} />}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path={`${url}/edit`}
|
||||||
|
component={() => <Edit repository={repository} />}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="column">
|
<div className="column">
|
||||||
<Navigation>
|
<Navigation>
|
||||||
<Section label={t("repository-root.actions-label")}>
|
<Section label={t("repository-root.actions-label")}>
|
||||||
|
<EditNavLink repository={repository} editUrl={`${url}/edit`} />
|
||||||
<DeleteNavAction repository={repository} delete={this.delete} />
|
<DeleteNavAction repository={repository} delete={this.delete} />
|
||||||
<NavLink to="/repos" label={t("repository-root.back-label")} />
|
<NavLink to="/repos" label={t("repository-root.back-label")} />
|
||||||
</Section>
|
</Section>
|
||||||
|
|||||||
@@ -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_FAILURE = `${CREATE_REPO}_${types.FAILURE_SUFFIX}`;
|
||||||
export const CREATE_REPO_RESET = `${CREATE_REPO}_${types.RESET_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 = "scm/repos/DELETE_REPO";
|
||||||
export const DELETE_REPO_PENDING = `${DELETE_REPO}_${types.PENDING_SUFFIX}`;
|
export const DELETE_REPO_PENDING = `${DELETE_REPO}_${types.PENDING_SUFFIX}`;
|
||||||
export const DELETE_REPO_SUCCESS = `${DELETE_REPO}_${types.SUCCESS_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
|
// delete
|
||||||
|
|
||||||
export function deleteRepo(repository: Repository, callback?: () => void) {
|
export function deleteRepo(repository: Repository, callback?: () => void) {
|
||||||
@@ -288,6 +341,8 @@ export default function reducer(
|
|||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case FETCH_REPOS_SUCCESS:
|
case FETCH_REPOS_SUCCESS:
|
||||||
return normalizeByNamespaceAndName(action.payload);
|
return normalizeByNamespaceAndName(action.payload);
|
||||||
|
case MODIFY_REPO_SUCCESS:
|
||||||
|
return reducerByNames(state, action.payload);
|
||||||
case FETCH_REPO_SUCCESS:
|
case FETCH_REPO_SUCCESS:
|
||||||
return reducerByNames(state, action.payload);
|
return reducerByNames(state, action.payload);
|
||||||
default:
|
default:
|
||||||
@@ -359,6 +414,22 @@ export function getCreateRepoFailure(state: Object) {
|
|||||||
return getFailure(state, CREATE_REPO);
|
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(
|
export function isDeleteRepoPending(
|
||||||
state: Object,
|
state: Object,
|
||||||
namespace: string,
|
namespace: string,
|
||||||
|
|||||||
@@ -37,7 +37,15 @@ import reducer, {
|
|||||||
DELETE_REPO_PENDING,
|
DELETE_REPO_PENDING,
|
||||||
DELETE_REPO_FAILURE,
|
DELETE_REPO_FAILURE,
|
||||||
isDeleteRepoPending,
|
isDeleteRepoPending,
|
||||||
getDeleteRepoFailure
|
getDeleteRepoFailure,
|
||||||
|
modifyRepo,
|
||||||
|
MODIFY_REPO_PENDING,
|
||||||
|
MODIFY_REPO_SUCCESS,
|
||||||
|
MODIFY_REPO_FAILURE,
|
||||||
|
MODIFY_REPO,
|
||||||
|
isModifyRepoPending,
|
||||||
|
getModifyRepoFailure,
|
||||||
|
modifyRepoSuccess
|
||||||
} from "./repos";
|
} from "./repos";
|
||||||
import type { Repository, RepositoryCollection } from "../types/Repositories";
|
import type { Repository, RepositoryCollection } from "../types/Repositories";
|
||||||
|
|
||||||
@@ -485,6 +493,64 @@ describe("repos fetch", () => {
|
|||||||
expect(actions[1].payload.error).toBeDefined();
|
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", () => {
|
describe("repos reducer", () => {
|
||||||
@@ -521,6 +587,18 @@ describe("repos reducer", () => {
|
|||||||
const newState = reducer({}, fetchRepoSuccess(slartiFjords));
|
const newState = reducer({}, fetchRepoSuccess(slartiFjords));
|
||||||
expect(newState.byNames["slarti/fjords"]).toBe(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", () => {
|
describe("repos selectors", () => {
|
||||||
@@ -635,6 +713,36 @@ describe("repos selectors", () => {
|
|||||||
expect(getCreateRepoFailure({})).toBe(undefined);
|
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
|
// delete
|
||||||
|
|
||||||
it("should return true, when delete repo is pending", () => {
|
it("should return true, when delete repo is pending", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user