mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-14 01:15:44 +01:00
merge 2.0.0-m3
This commit is contained in:
40
scm-ui/src/repos/components/DropDown.js
Normal file
40
scm-ui/src/repos/components/DropDown.js
Normal file
@@ -0,0 +1,40 @@
|
||||
// @flow
|
||||
|
||||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
type Props = {
|
||||
options: string[],
|
||||
optionSelected: string => void,
|
||||
preselectedOption?: string,
|
||||
className: any
|
||||
};
|
||||
|
||||
class DropDown extends React.Component<Props> {
|
||||
render() {
|
||||
const { options, preselectedOption, className } = this.props;
|
||||
return (
|
||||
<div className={classNames(className, "select")}>
|
||||
<select
|
||||
value={preselectedOption ? preselectedOption : ""}
|
||||
onChange={this.change}
|
||||
>
|
||||
<option key="" />
|
||||
{options.map(option => {
|
||||
return (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
change = (event: SyntheticInputEvent<HTMLSelectElement>) => {
|
||||
this.props.optionSelected(event.target.value);
|
||||
};
|
||||
}
|
||||
|
||||
export default DropDown;
|
||||
@@ -4,7 +4,6 @@ import "../../tests/enzyme";
|
||||
import "../../tests/i18n";
|
||||
import ReactRouterEnzymeContext from "react-router-enzyme-context";
|
||||
import PermissionsNavLink from "./PermissionsNavLink";
|
||||
import EditNavLink from "./EditNavLink";
|
||||
|
||||
describe("PermissionsNavLink", () => {
|
||||
const options = new ReactRouterEnzymeContext();
|
||||
|
||||
37
scm-ui/src/repos/components/changesets/ChangesetAuthor.js
Normal file
37
scm-ui/src/repos/components/changesets/ChangesetAuthor.js
Normal file
@@ -0,0 +1,37 @@
|
||||
//@flow
|
||||
|
||||
import React from "react";
|
||||
import type { Changeset } from "@scm-manager/ui-types";
|
||||
|
||||
type Props = {
|
||||
changeset: Changeset
|
||||
};
|
||||
|
||||
export default class ChangesetAuthor extends React.Component<Props> {
|
||||
render() {
|
||||
const { changeset } = this.props;
|
||||
if (!changeset.author) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { name } = changeset.author;
|
||||
return (
|
||||
<>
|
||||
{name} {this.renderMail()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
renderMail() {
|
||||
const { mail } = this.props.changeset.author;
|
||||
if (mail) {
|
||||
return (
|
||||
<a className="is-hidden-mobile" href={"mailto:" + mail}>
|
||||
<
|
||||
{mail}
|
||||
>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
30
scm-ui/src/repos/components/changesets/ChangesetAvatar.js
Normal file
30
scm-ui/src/repos/components/changesets/ChangesetAvatar.js
Normal file
@@ -0,0 +1,30 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import { ExtensionPoint } from "@scm-manager/ui-extensions";
|
||||
import type { Changeset } from "@scm-manager/ui-types";
|
||||
|
||||
type Props = {
|
||||
changeset: Changeset
|
||||
};
|
||||
|
||||
class ChangesetAvatar extends React.Component<Props> {
|
||||
render() {
|
||||
const { changeset } = this.props;
|
||||
return (
|
||||
<ExtensionPoint
|
||||
name="repos.changeset-table.information"
|
||||
renderAll={true}
|
||||
props={{ changeset }}
|
||||
>
|
||||
{/* extension should render something like this: */}
|
||||
{/* <div className="image is-64x64"> */}
|
||||
{/* <figure className="media-left"> */}
|
||||
{/* <Image src="/some/image.jpg" alt="Logo" /> */}
|
||||
{/* </figure> */}
|
||||
{/* </div> */}
|
||||
</ExtensionPoint>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ChangesetAvatar;
|
||||
25
scm-ui/src/repos/components/changesets/ChangesetId.js
Normal file
25
scm-ui/src/repos/components/changesets/ChangesetId.js
Normal file
@@ -0,0 +1,25 @@
|
||||
//@flow
|
||||
|
||||
import { Link } from "react-router-dom";
|
||||
import React from "react";
|
||||
import type { Repository, Changeset } from "@scm-manager/ui-types";
|
||||
|
||||
type Props = {
|
||||
repository: Repository,
|
||||
changeset: Changeset
|
||||
};
|
||||
|
||||
export default class ChangesetId extends React.Component<Props> {
|
||||
render() {
|
||||
const { repository, changeset } = this.props;
|
||||
return (
|
||||
<Link
|
||||
to={`/repo/${repository.namespace}/${repository.name}/changeset/${
|
||||
changeset.id
|
||||
}`}
|
||||
>
|
||||
{changeset.id.substr(0, 7)}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
}
|
||||
28
scm-ui/src/repos/components/changesets/ChangesetList.js
Normal file
28
scm-ui/src/repos/components/changesets/ChangesetList.js
Normal file
@@ -0,0 +1,28 @@
|
||||
// @flow
|
||||
import ChangesetRow from "./ChangesetRow";
|
||||
import React from "react";
|
||||
import type { Changeset, Repository } from "@scm-manager/ui-types";
|
||||
import classNames from "classnames";
|
||||
|
||||
type Props = {
|
||||
repository: Repository,
|
||||
changesets: Changeset[]
|
||||
};
|
||||
|
||||
class ChangesetList extends React.Component<Props> {
|
||||
render() {
|
||||
const { repository, changesets } = this.props;
|
||||
const content = changesets.map(changeset => {
|
||||
return (
|
||||
<ChangesetRow
|
||||
key={changeset.id}
|
||||
repository={repository}
|
||||
changeset={changeset}
|
||||
/>
|
||||
);
|
||||
});
|
||||
return <div className={classNames("box")}>{content}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
export default ChangesetList;
|
||||
90
scm-ui/src/repos/components/changesets/ChangesetRow.js
Normal file
90
scm-ui/src/repos/components/changesets/ChangesetRow.js
Normal file
@@ -0,0 +1,90 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import type { Changeset, Repository, Tag } from "@scm-manager/ui-types";
|
||||
import classNames from "classnames";
|
||||
import { translate, Interpolate } from "react-i18next";
|
||||
import ChangesetAvatar from "./ChangesetAvatar";
|
||||
import ChangesetId from "./ChangesetId";
|
||||
import injectSheet from "react-jss";
|
||||
import { DateFromNow } from "@scm-manager/ui-components";
|
||||
import ChangesetAuthor from "./ChangesetAuthor";
|
||||
import ChangesetTag from "./ChangesetTag";
|
||||
import { compose } from "redux";
|
||||
|
||||
const styles = {
|
||||
pointer: {
|
||||
cursor: "pointer"
|
||||
},
|
||||
changesetGroup: {
|
||||
marginBottom: "1em"
|
||||
},
|
||||
withOverflow: {
|
||||
overflow: "auto"
|
||||
}
|
||||
};
|
||||
|
||||
type Props = {
|
||||
repository: Repository,
|
||||
changeset: Changeset,
|
||||
t: any,
|
||||
classes: any
|
||||
};
|
||||
|
||||
class ChangesetRow extends React.Component<Props> {
|
||||
createLink = (changeset: Changeset) => {
|
||||
const { repository } = this.props;
|
||||
return <ChangesetId changeset={changeset} repository={repository} />;
|
||||
};
|
||||
|
||||
getTags = () => {
|
||||
const { changeset } = this.props;
|
||||
return changeset._embedded.tags || [];
|
||||
};
|
||||
|
||||
render() {
|
||||
const { changeset, classes } = this.props;
|
||||
const changesetLink = this.createLink(changeset);
|
||||
const dateFromNow = <DateFromNow date={changeset.date} />;
|
||||
const authorLine = <ChangesetAuthor changeset={changeset} />;
|
||||
|
||||
return (
|
||||
<article className={classNames("media", classes.inner)}>
|
||||
<ChangesetAvatar changeset={changeset} />
|
||||
<div className={classNames("media-content", classes.withOverflow)}>
|
||||
<div className="content">
|
||||
<p className="is-ellipsis-overflow">
|
||||
{changeset.description}
|
||||
<br />
|
||||
<Interpolate
|
||||
i18nKey="changesets.changeset.summary"
|
||||
id={changesetLink}
|
||||
time={dateFromNow}
|
||||
/>
|
||||
</p>{" "}
|
||||
<div className="is-size-7">{authorLine}</div>
|
||||
</div>
|
||||
</div>
|
||||
{this.renderTags()}
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
renderTags = () => {
|
||||
const tags = this.getTags();
|
||||
if (tags.length > 0) {
|
||||
return (
|
||||
<div className="media-right">
|
||||
{tags.map((tag: Tag) => {
|
||||
return <ChangesetTag key={tag.name} tag={tag} />;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
export default compose(
|
||||
injectSheet(styles),
|
||||
translate("repos")
|
||||
)(ChangesetRow);
|
||||
32
scm-ui/src/repos/components/changesets/ChangesetTag.js
Normal file
32
scm-ui/src/repos/components/changesets/ChangesetTag.js
Normal file
@@ -0,0 +1,32 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import type { Tag } from "@scm-manager/ui-types";
|
||||
import injectSheet from "react-jss";
|
||||
import classNames from "classnames";
|
||||
|
||||
const styles = {
|
||||
spacing: {
|
||||
marginRight: "4px"
|
||||
}
|
||||
};
|
||||
|
||||
type Props = {
|
||||
tag: Tag,
|
||||
|
||||
// context props
|
||||
classes: Object
|
||||
};
|
||||
|
||||
class ChangesetTag extends React.Component<Props> {
|
||||
render() {
|
||||
const { tag, classes } = this.props;
|
||||
return (
|
||||
<span className="tag is-info">
|
||||
<span className={classNames("fa", "fa-tag", classes.spacing)} />{" "}
|
||||
{tag.name}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default injectSheet(styles)(ChangesetTag);
|
||||
@@ -1,9 +1,9 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import {Link} from "react-router-dom";
|
||||
import injectSheet from "react-jss";
|
||||
import type { Repository } from "@scm-manager/ui-types";
|
||||
import { DateFromNow } from "@scm-manager/ui-components";
|
||||
import type {Repository} from "@scm-manager/ui-types";
|
||||
import {DateFromNow} from "@scm-manager/ui-components";
|
||||
import RepositoryEntryLink from "./RepositoryEntryLink";
|
||||
import classNames from "classnames";
|
||||
import RepositoryAvatar from "./RepositoryAvatar";
|
||||
@@ -45,7 +45,7 @@ class RepositoryEntry extends React.Component<Props> {
|
||||
return (
|
||||
<RepositoryEntryLink
|
||||
iconClass="fa-code-branch"
|
||||
to={repositoryLink + "/changesets"}
|
||||
to={repositoryLink + "/history"}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -67,10 +67,7 @@ class RepositoryEntry extends React.Component<Props> {
|
||||
renderModifyLink = (repository: Repository, repositoryLink: string) => {
|
||||
if (repository._links["update"]) {
|
||||
return (
|
||||
<RepositoryEntryLink
|
||||
iconClass="fa-cog"
|
||||
to={repositoryLink + "/modify"}
|
||||
/>
|
||||
<RepositoryEntryLink iconClass="fa-cog" to={repositoryLink + "/edit"} />
|
||||
);
|
||||
}
|
||||
return null;
|
||||
|
||||
139
scm-ui/src/repos/containers/BranchRoot.js
Normal file
139
scm-ui/src/repos/containers/BranchRoot.js
Normal file
@@ -0,0 +1,139 @@
|
||||
// @flow
|
||||
|
||||
import React from "react";
|
||||
import type { Branch, Repository } from "@scm-manager/ui-types";
|
||||
import { Route, withRouter } from "react-router-dom";
|
||||
import Changesets from "./Changesets";
|
||||
import BranchSelector from "./BranchSelector";
|
||||
import { connect } from "react-redux";
|
||||
import { ErrorNotification, Loading } from "@scm-manager/ui-components";
|
||||
import {
|
||||
fetchBranches,
|
||||
getBranches,
|
||||
getFetchBranchesFailure,
|
||||
isFetchBranchesPending
|
||||
} from "../modules/branches";
|
||||
import { compose } from "redux";
|
||||
|
||||
type Props = {
|
||||
repository: Repository,
|
||||
baseUrl: string,
|
||||
selected: string,
|
||||
baseUrlWithBranch: string,
|
||||
baseUrlWithoutBranch: string,
|
||||
|
||||
// State props
|
||||
branches: Branch[],
|
||||
loading: boolean,
|
||||
error: Error,
|
||||
|
||||
// Dispatch props
|
||||
fetchBranches: Repository => void,
|
||||
|
||||
// Context props
|
||||
history: any, // TODO flow type
|
||||
match: any
|
||||
};
|
||||
|
||||
class BranchRoot extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
this.props.fetchBranches(this.props.repository);
|
||||
}
|
||||
|
||||
stripEndingSlash = (url: string) => {
|
||||
if (url.endsWith("/")) {
|
||||
return url.substring(0, url.length - 1);
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
branchSelected = (branch?: Branch) => {
|
||||
let url;
|
||||
if (branch) {
|
||||
url = `${this.props.baseUrlWithBranch}/${encodeURIComponent(
|
||||
branch.name
|
||||
)}/changesets/`;
|
||||
} else {
|
||||
url = `${this.props.baseUrlWithoutBranch}/`;
|
||||
}
|
||||
this.props.history.push(url);
|
||||
};
|
||||
|
||||
findSelectedBranch = () => {
|
||||
const { selected, branches } = this.props;
|
||||
return branches.find((branch: Branch) => branch.name === selected);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { repository, error, loading, match, branches } = this.props;
|
||||
|
||||
if (error) {
|
||||
return <ErrorNotification error={error} />;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (!repository || !branches) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const url = this.stripEndingSlash(match.url);
|
||||
const branch = this.findSelectedBranch();
|
||||
const changesets = <Changesets repository={repository} branch={branch} />;
|
||||
|
||||
return (
|
||||
<>
|
||||
{this.renderBranchSelector()}
|
||||
<Route path={`${url}/:page?`} component={() => changesets} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
renderBranchSelector = () => {
|
||||
const { repository, branches } = this.props;
|
||||
if (repository._links.branches) {
|
||||
return (
|
||||
<BranchSelector
|
||||
branches={branches}
|
||||
selected={(b: Branch) => {
|
||||
this.branchSelected(b);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
fetchBranches: (repo: Repository) => {
|
||||
dispatch(fetchBranches(repo));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: any, ownProps: Props) => {
|
||||
const { repository, match } = ownProps;
|
||||
const loading = isFetchBranchesPending(state, repository);
|
||||
const error = getFetchBranchesFailure(state, repository);
|
||||
const branches = getBranches(state, repository);
|
||||
const selected = decodeURIComponent(match.params.branch);
|
||||
|
||||
return {
|
||||
loading,
|
||||
error,
|
||||
branches,
|
||||
selected
|
||||
};
|
||||
};
|
||||
|
||||
export default compose(
|
||||
withRouter,
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)
|
||||
)(BranchRoot);
|
||||
78
scm-ui/src/repos/containers/BranchSelector.js
Normal file
78
scm-ui/src/repos/containers/BranchSelector.js
Normal file
@@ -0,0 +1,78 @@
|
||||
// @flow
|
||||
|
||||
import React from "react";
|
||||
import type { Branch } from "@scm-manager/ui-types";
|
||||
import DropDown from "../components/DropDown";
|
||||
import { translate } from "react-i18next";
|
||||
import injectSheet from "react-jss";
|
||||
import { compose } from "redux";
|
||||
import classNames from "classnames";
|
||||
|
||||
const styles = {
|
||||
zeroflex: {
|
||||
flexGrow: 0
|
||||
}
|
||||
};
|
||||
|
||||
type Props = {
|
||||
branches: Branch[], // TODO: Use generics?
|
||||
selected: (branch?: Branch) => void,
|
||||
|
||||
// context props
|
||||
classes: Object,
|
||||
t: string => string
|
||||
};
|
||||
|
||||
type State = { selectedBranch?: Branch };
|
||||
|
||||
class BranchSelector extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
render() {
|
||||
const { branches, classes, t } = this.props;
|
||||
|
||||
if (branches) {
|
||||
return (
|
||||
<div className="box field is-horizontal">
|
||||
<div
|
||||
className={classNames("field-label", "is-normal", classes.zeroflex)}
|
||||
>
|
||||
<label className="label">{t("branch-selector.label")}</label>
|
||||
</div>
|
||||
<div className="field-body">
|
||||
<div className="field is-narrow">
|
||||
<div className="control">
|
||||
<DropDown
|
||||
className="is-fullwidth"
|
||||
options={branches.map(b => b.name)}
|
||||
optionSelected={this.branchSelected}
|
||||
preselectedOption={
|
||||
this.state.selectedBranch
|
||||
? this.state.selectedBranch.name
|
||||
: ""
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
branchSelected = (branchName: string) => {
|
||||
const { branches, selected } = this.props;
|
||||
const branch = branches.find(b => b.name === branchName);
|
||||
|
||||
selected(branch);
|
||||
this.setState({ selectedBranch: branch });
|
||||
};
|
||||
}
|
||||
|
||||
export default compose(
|
||||
injectSheet(styles),
|
||||
translate("repos")
|
||||
)(BranchSelector);
|
||||
115
scm-ui/src/repos/containers/Changesets.js
Normal file
115
scm-ui/src/repos/containers/Changesets.js
Normal file
@@ -0,0 +1,115 @@
|
||||
// @flow
|
||||
|
||||
import React from "react";
|
||||
import { withRouter } from "react-router-dom";
|
||||
import type {
|
||||
Branch,
|
||||
Changeset,
|
||||
PagedCollection,
|
||||
Repository
|
||||
} from "@scm-manager/ui-types";
|
||||
import {
|
||||
fetchChangesets,
|
||||
getChangesets,
|
||||
getFetchChangesetsFailure,
|
||||
isFetchChangesetsPending,
|
||||
selectListAsCollection
|
||||
} from "../modules/changesets";
|
||||
|
||||
import { connect } from "react-redux";
|
||||
import ChangesetList from "../components/changesets/ChangesetList";
|
||||
import {
|
||||
ErrorNotification,
|
||||
LinkPaginator,
|
||||
Loading,
|
||||
getPageFromMatch
|
||||
} from "@scm-manager/ui-components";
|
||||
import { compose } from "redux";
|
||||
|
||||
type Props = {
|
||||
repository: Repository,
|
||||
branch: Branch,
|
||||
page: number,
|
||||
|
||||
// State props
|
||||
changesets: Changeset[],
|
||||
list: PagedCollection,
|
||||
loading: boolean,
|
||||
error: Error,
|
||||
|
||||
// Dispatch props
|
||||
fetchChangesets: (Repository, Branch, number) => void,
|
||||
|
||||
// context props
|
||||
match: any
|
||||
};
|
||||
|
||||
class Changesets extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
const { fetchChangesets, repository, branch, page } = this.props;
|
||||
|
||||
fetchChangesets(repository, branch, page);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { changesets, loading, error } = this.props;
|
||||
|
||||
if (error) {
|
||||
return <ErrorNotification error={error} />;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (!changesets || changesets.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{this.renderList()}
|
||||
{this.renderPaginator()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
renderList = () => {
|
||||
const { repository, changesets } = this.props;
|
||||
return <ChangesetList repository={repository} changesets={changesets} />;
|
||||
};
|
||||
|
||||
renderPaginator = () => {
|
||||
const { page, list } = this.props;
|
||||
if (list) {
|
||||
return <LinkPaginator page={page} collection={list} />;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
fetchChangesets: (repo: Repository, branch: Branch, page: number) => {
|
||||
dispatch(fetchChangesets(repo, branch, page));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: any, ownProps: Props) => {
|
||||
const { repository, branch, match } = ownProps;
|
||||
const changesets = getChangesets(state, repository, branch);
|
||||
const loading = isFetchChangesetsPending(state, repository, branch);
|
||||
const error = getFetchChangesetsFailure(state, repository, branch);
|
||||
const list = selectListAsCollection(state, repository, branch);
|
||||
const page = getPageFromMatch(match);
|
||||
|
||||
return { changesets, list, page, loading, error };
|
||||
};
|
||||
|
||||
export default compose(
|
||||
withRouter,
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)
|
||||
)(Changesets);
|
||||
@@ -8,14 +8,14 @@ import {
|
||||
isFetchRepoPending
|
||||
} from "../modules/repos";
|
||||
import { connect } from "react-redux";
|
||||
import { Route } from "react-router-dom";
|
||||
import { Route, Switch } from "react-router-dom";
|
||||
import type { Repository } from "@scm-manager/ui-types";
|
||||
import {
|
||||
Page,
|
||||
Loading,
|
||||
ErrorPage,
|
||||
Loading,
|
||||
Navigation,
|
||||
NavLink,
|
||||
Page,
|
||||
Section
|
||||
} from "@scm-manager/ui-components";
|
||||
import { translate } from "react-i18next";
|
||||
@@ -26,6 +26,7 @@ import Permissions from "../permissions/containers/Permissions";
|
||||
|
||||
import type { History } from "history";
|
||||
import EditNavLink from "../components/EditNavLink";
|
||||
import BranchRoot from "./BranchRoot";
|
||||
import PermissionsNavLink from "../components/PermissionsNavLink";
|
||||
import { getRepositoriesLink } from "../../modules/indexResource";
|
||||
|
||||
@@ -73,6 +74,12 @@ class RepositoryRoot extends React.Component<Props> {
|
||||
this.props.deleteRepo(repository, this.deleted);
|
||||
};
|
||||
|
||||
matches = (route: any) => {
|
||||
const url = this.matchedUrl();
|
||||
const regex = new RegExp(`${url}(/branches)?/?[^/]*/changesets?.*`);
|
||||
return route.location.pathname.match(regex);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { loading, error, repository, t } = this.props;
|
||||
|
||||
@@ -91,39 +98,66 @@ class RepositoryRoot extends React.Component<Props> {
|
||||
}
|
||||
|
||||
const url = this.matchedUrl();
|
||||
|
||||
return (
|
||||
<Page title={repository.namespace + "/" + repository.name}>
|
||||
<div className="columns">
|
||||
<div className="column is-three-quarters">
|
||||
<Route
|
||||
path={url}
|
||||
exact
|
||||
component={() => <RepositoryDetails repository={repository} />}
|
||||
/>
|
||||
<Route
|
||||
path={`${url}/edit`}
|
||||
component={() => <Edit repository={repository} />}
|
||||
/>
|
||||
<Route
|
||||
path={`${url}/permissions`}
|
||||
render={props => (
|
||||
<Permissions
|
||||
namespace={this.props.repository.namespace}
|
||||
repoName={this.props.repository.name}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Switch>
|
||||
<Route
|
||||
path={url}
|
||||
exact
|
||||
component={() => <RepositoryDetails repository={repository} />}
|
||||
/>
|
||||
<Route
|
||||
path={`${url}/edit`}
|
||||
component={() => <Edit repository={repository} />}
|
||||
/>
|
||||
<Route
|
||||
path={`${url}/permissions`}
|
||||
render={props => (
|
||||
<Permissions
|
||||
namespace={this.props.repository.namespace}
|
||||
repoName={this.props.repository.name}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path={`${url}/changesets`}
|
||||
render={() => (
|
||||
<BranchRoot
|
||||
repository={repository}
|
||||
baseUrlWithBranch={`${url}/branches`}
|
||||
baseUrlWithoutBranch={`${url}/changesets`}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path={`${url}/branches/:branch/changesets`}
|
||||
render={() => (
|
||||
<BranchRoot
|
||||
repository={repository}
|
||||
baseUrlWithBranch={`${url}/branches`}
|
||||
baseUrlWithoutBranch={`${url}/changesets`}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
</div>
|
||||
<div className="column">
|
||||
<Navigation>
|
||||
<Section label={t("repository-root.navigation-label")}>
|
||||
<NavLink to={url} label={t("repository-root.information")} />
|
||||
<NavLink
|
||||
activeOnlyWhenExact={false}
|
||||
to={`${url}/changesets/`}
|
||||
label={t("repository-root.history")}
|
||||
activeWhenMatch={this.matches}
|
||||
/>
|
||||
<EditNavLink repository={repository} editUrl={`${url}/edit`} />
|
||||
<PermissionsNavLink
|
||||
permissionUrl={`${url}/permissions`}
|
||||
repository={repository}
|
||||
/>
|
||||
<EditNavLink repository={repository} editUrl={`${url}/edit`} />
|
||||
</Section>
|
||||
<Section label={t("repository-root.actions-label")}>
|
||||
<DeleteNavAction repository={repository} delete={this.delete} />
|
||||
|
||||
134
scm-ui/src/repos/modules/branches.js
Normal file
134
scm-ui/src/repos/modules/branches.js
Normal file
@@ -0,0 +1,134 @@
|
||||
// @flow
|
||||
import {
|
||||
FAILURE_SUFFIX,
|
||||
PENDING_SUFFIX,
|
||||
SUCCESS_SUFFIX
|
||||
} from "../../modules/types";
|
||||
import { apiClient } from "@scm-manager/ui-components";
|
||||
import type { Action, Branch, Repository } from "@scm-manager/ui-types";
|
||||
import { isPending } from "../../modules/pending";
|
||||
import { getFailure } from "../../modules/failure";
|
||||
|
||||
export const FETCH_BRANCHES = "scm/repos/FETCH_BRANCHES";
|
||||
export const FETCH_BRANCHES_PENDING = `${FETCH_BRANCHES}_${PENDING_SUFFIX}`;
|
||||
export const FETCH_BRANCHES_SUCCESS = `${FETCH_BRANCHES}_${SUCCESS_SUFFIX}`;
|
||||
export const FETCH_BRANCHES_FAILURE = `${FETCH_BRANCHES}_${FAILURE_SUFFIX}`;
|
||||
|
||||
// Fetching branches
|
||||
|
||||
export function fetchBranches(repository: Repository) {
|
||||
if (!repository._links.branches) {
|
||||
return {
|
||||
type: FETCH_BRANCHES_SUCCESS,
|
||||
payload: { repository, data: {} },
|
||||
itemId: createKey(repository)
|
||||
};
|
||||
}
|
||||
|
||||
return function(dispatch: any) {
|
||||
dispatch(fetchBranchesPending(repository));
|
||||
return apiClient
|
||||
.get(repository._links.branches.href)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
dispatch(fetchBranchesSuccess(data, repository));
|
||||
})
|
||||
.catch(error => {
|
||||
dispatch(fetchBranchesFailure(repository, error));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// Action creators
|
||||
export function fetchBranchesPending(repository: Repository) {
|
||||
return {
|
||||
type: FETCH_BRANCHES_PENDING,
|
||||
payload: { repository },
|
||||
itemId: createKey(repository)
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchBranchesSuccess(data: string, repository: Repository) {
|
||||
return {
|
||||
type: FETCH_BRANCHES_SUCCESS,
|
||||
payload: { data, repository },
|
||||
itemId: createKey(repository)
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchBranchesFailure(repository: Repository, error: Error) {
|
||||
return {
|
||||
type: FETCH_BRANCHES_FAILURE,
|
||||
payload: { error, repository },
|
||||
itemId: createKey(repository)
|
||||
};
|
||||
}
|
||||
|
||||
// Reducers
|
||||
|
||||
type State = { [string]: Branch[] };
|
||||
|
||||
export default function reducer(
|
||||
state: State = {},
|
||||
action: Action = { type: "UNKNOWN" }
|
||||
): State {
|
||||
if (!action.payload) {
|
||||
return state;
|
||||
}
|
||||
const payload = action.payload;
|
||||
switch (action.type) {
|
||||
case FETCH_BRANCHES_SUCCESS:
|
||||
const key = createKey(payload.repository);
|
||||
return {
|
||||
...state,
|
||||
[key]: extractBranchesFromPayload(payload.data)
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
function extractBranchesFromPayload(payload: any) {
|
||||
if (payload._embedded && payload._embedded.branches) {
|
||||
return payload._embedded.branches;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Selectors
|
||||
|
||||
export function getBranches(state: Object, repository: Repository) {
|
||||
const key = createKey(repository);
|
||||
if (state.branches[key]) {
|
||||
return state.branches[key];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getBranch(
|
||||
state: Object,
|
||||
repository: Repository,
|
||||
name: string
|
||||
): ?Branch {
|
||||
const key = createKey(repository);
|
||||
if (state.branches[key]) {
|
||||
return state.branches[key].find((b: Branch) => b.name === name);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function isFetchBranchesPending(
|
||||
state: Object,
|
||||
repository: Repository
|
||||
): boolean {
|
||||
return isPending(state, FETCH_BRANCHES, createKey(repository));
|
||||
}
|
||||
|
||||
export function getFetchBranchesFailure(state: Object, repository: Repository) {
|
||||
return getFailure(state, FETCH_BRANCHES, createKey(repository));
|
||||
}
|
||||
|
||||
function createKey(repository: Repository): string {
|
||||
const { namespace, name } = repository;
|
||||
return `${namespace}/${name}`;
|
||||
}
|
||||
195
scm-ui/src/repos/modules/branches.test.js
Normal file
195
scm-ui/src/repos/modules/branches.test.js
Normal file
@@ -0,0 +1,195 @@
|
||||
import configureMockStore from "redux-mock-store";
|
||||
import thunk from "redux-thunk";
|
||||
import fetchMock from "fetch-mock";
|
||||
import reducer, {
|
||||
FETCH_BRANCHES,
|
||||
FETCH_BRANCHES_FAILURE,
|
||||
FETCH_BRANCHES_PENDING,
|
||||
FETCH_BRANCHES_SUCCESS,
|
||||
fetchBranches,
|
||||
getBranch,
|
||||
getBranches,
|
||||
getFetchBranchesFailure,
|
||||
isFetchBranchesPending
|
||||
} from "./branches";
|
||||
|
||||
const namespace = "foo";
|
||||
const name = "bar";
|
||||
const key = namespace + "/" + name;
|
||||
const repository = {
|
||||
namespace: "foo",
|
||||
name: "bar",
|
||||
_links: {
|
||||
branches: {
|
||||
href: "http://scm/api/rest/v2/repositories/foo/bar/branches"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const branch1 = { name: "branch1", revision: "revision1" };
|
||||
const branch2 = { name: "branch2", revision: "revision2" };
|
||||
const branch3 = { name: "branch3", revision: "revision3" };
|
||||
|
||||
describe("branches", () => {
|
||||
describe("fetch branches", () => {
|
||||
const URL = "http://scm/api/rest/v2/repositories/foo/bar/branches";
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
|
||||
afterEach(() => {
|
||||
fetchMock.reset();
|
||||
fetchMock.restore();
|
||||
});
|
||||
|
||||
it("should fetch branches", () => {
|
||||
const collection = {};
|
||||
|
||||
fetchMock.getOnce(URL, "{}");
|
||||
|
||||
const expectedActions = [
|
||||
{
|
||||
type: FETCH_BRANCHES_PENDING,
|
||||
payload: { repository },
|
||||
itemId: key
|
||||
},
|
||||
{
|
||||
type: FETCH_BRANCHES_SUCCESS,
|
||||
payload: { data: collection, repository },
|
||||
itemId: key
|
||||
}
|
||||
];
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchBranches(repository)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail fetching branches on HTTP 500", () => {
|
||||
const collection = {};
|
||||
|
||||
fetchMock.getOnce(URL, 500);
|
||||
|
||||
const expectedActions = [
|
||||
{
|
||||
type: FETCH_BRANCHES_PENDING,
|
||||
payload: { repository },
|
||||
itemId: key
|
||||
},
|
||||
{
|
||||
type: FETCH_BRANCHES_FAILURE,
|
||||
payload: { error: collection, repository },
|
||||
itemId: key
|
||||
}
|
||||
];
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchBranches(repository)).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: {
|
||||
repository,
|
||||
data: branches
|
||||
}
|
||||
};
|
||||
|
||||
it("should update state according to successful fetch", () => {
|
||||
const newState = reducer({}, action);
|
||||
expect(newState).toBeDefined();
|
||||
expect(newState[key]).toBeDefined();
|
||||
expect(newState[key]).toContain(branch1);
|
||||
expect(newState[key]).toContain(branch2);
|
||||
});
|
||||
|
||||
it("should not delete existing branches from state", () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
describe("branch selectors", () => {
|
||||
const error = new Error("Something went wrong");
|
||||
|
||||
const state = {
|
||||
branches: {
|
||||
[key]: [branch1, branch2]
|
||||
}
|
||||
};
|
||||
|
||||
it("should return true, when fetching branches is pending", () => {
|
||||
const state = {
|
||||
pending: {
|
||||
[FETCH_BRANCHES + "/foo/bar"]: true
|
||||
}
|
||||
};
|
||||
|
||||
expect(isFetchBranchesPending(state, repository)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should return branches", () => {
|
||||
const branches = getBranches(state, repository);
|
||||
expect(branches.length).toEqual(2);
|
||||
expect(branches).toContain(branch1);
|
||||
expect(branches).toContain(branch2);
|
||||
});
|
||||
|
||||
it("should return always the same reference for branches", () => {
|
||||
const one = getBranches(state, repository);
|
||||
const two = getBranches(state, repository);
|
||||
expect(one).toBe(two);
|
||||
});
|
||||
|
||||
it("should return null, if no branches for the repository available", () => {
|
||||
const branches = getBranches({ branches: {} }, repository);
|
||||
expect(branches).toBeNull();
|
||||
});
|
||||
|
||||
it("should return single branch by name", () => {
|
||||
const branch = getBranch(state, repository, "branch1");
|
||||
expect(branch).toEqual(branch1);
|
||||
});
|
||||
|
||||
it("should return same reference for single branch by name", () => {
|
||||
const one = getBranch(state, repository, "branch1");
|
||||
const two = getBranch(state, repository, "branch1");
|
||||
expect(one).toBe(two);
|
||||
});
|
||||
|
||||
it("should return undefined if branch does not exist", () => {
|
||||
const branch = getBranch(state, repository, "branch42");
|
||||
expect(branch).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should return error if fetching branches failed", () => {
|
||||
const state = {
|
||||
failure: {
|
||||
[FETCH_BRANCHES + "/foo/bar"]: error
|
||||
}
|
||||
};
|
||||
|
||||
expect(getFetchBranchesFailure(state, repository)).toEqual(error);
|
||||
});
|
||||
|
||||
it("should return false if fetching branches did not fail", () => {
|
||||
expect(getFetchBranchesFailure({}, repository)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
230
scm-ui/src/repos/modules/changesets.js
Normal file
230
scm-ui/src/repos/modules/changesets.js
Normal file
@@ -0,0 +1,230 @@
|
||||
// @flow
|
||||
|
||||
import {
|
||||
FAILURE_SUFFIX,
|
||||
PENDING_SUFFIX,
|
||||
SUCCESS_SUFFIX
|
||||
} from "../../modules/types";
|
||||
import { apiClient } from "@scm-manager/ui-components";
|
||||
import { isPending } from "../../modules/pending";
|
||||
import { getFailure } from "../../modules/failure";
|
||||
import type {
|
||||
Action,
|
||||
Branch,
|
||||
PagedCollection,
|
||||
Repository
|
||||
} from "@scm-manager/ui-types";
|
||||
|
||||
export const FETCH_CHANGESETS = "scm/repos/FETCH_CHANGESETS";
|
||||
export const FETCH_CHANGESETS_PENDING = `${FETCH_CHANGESETS}_${PENDING_SUFFIX}`;
|
||||
export const FETCH_CHANGESETS_SUCCESS = `${FETCH_CHANGESETS}_${SUCCESS_SUFFIX}`;
|
||||
export const FETCH_CHANGESETS_FAILURE = `${FETCH_CHANGESETS}_${FAILURE_SUFFIX}`;
|
||||
|
||||
//TODO: Content type
|
||||
// actions
|
||||
|
||||
export function fetchChangesets(
|
||||
repository: Repository,
|
||||
branch?: Branch,
|
||||
page?: number
|
||||
) {
|
||||
const link = createChangesetsLink(repository, branch, page);
|
||||
|
||||
return function(dispatch: any) {
|
||||
dispatch(fetchChangesetsPending(repository, branch));
|
||||
return apiClient
|
||||
.get(link)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
dispatch(fetchChangesetsSuccess(repository, branch, data));
|
||||
})
|
||||
.catch(cause => {
|
||||
dispatch(fetchChangesetsFailure(repository, branch, cause));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function createChangesetsLink(
|
||||
repository: Repository,
|
||||
branch?: Branch,
|
||||
page?: number
|
||||
) {
|
||||
let link = repository._links.changesets.href;
|
||||
|
||||
if (branch) {
|
||||
link = branch._links.history.href;
|
||||
}
|
||||
|
||||
if (page) {
|
||||
link = link + `?page=${page - 1}`;
|
||||
}
|
||||
return link;
|
||||
}
|
||||
|
||||
export function fetchChangesetsPending(
|
||||
repository: Repository,
|
||||
branch?: Branch
|
||||
): Action {
|
||||
const itemId = createItemId(repository, branch);
|
||||
|
||||
return {
|
||||
type: FETCH_CHANGESETS_PENDING,
|
||||
itemId
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchChangesetsSuccess(
|
||||
repository: Repository,
|
||||
branch?: Branch,
|
||||
changesets: any
|
||||
): Action {
|
||||
return {
|
||||
type: FETCH_CHANGESETS_SUCCESS,
|
||||
payload: changesets,
|
||||
itemId: createItemId(repository, branch)
|
||||
};
|
||||
}
|
||||
|
||||
function fetchChangesetsFailure(
|
||||
repository: Repository,
|
||||
branch?: Branch,
|
||||
error: Error
|
||||
): Action {
|
||||
return {
|
||||
type: FETCH_CHANGESETS_FAILURE,
|
||||
payload: {
|
||||
repository,
|
||||
error,
|
||||
branch
|
||||
},
|
||||
itemId: createItemId(repository, branch)
|
||||
};
|
||||
}
|
||||
|
||||
function createItemId(repository: Repository, branch?: Branch): string {
|
||||
const { namespace, name } = repository;
|
||||
let itemId = namespace + "/" + name;
|
||||
if (branch) {
|
||||
itemId = itemId + "/" + branch.name;
|
||||
}
|
||||
return itemId;
|
||||
}
|
||||
|
||||
// reducer
|
||||
export default function reducer(
|
||||
state: any = {},
|
||||
action: Action = { type: "UNKNOWN" }
|
||||
): Object {
|
||||
if (!action.payload) {
|
||||
return state;
|
||||
}
|
||||
const payload = action.payload;
|
||||
switch (action.type) {
|
||||
case FETCH_CHANGESETS_SUCCESS:
|
||||
const changesets = payload._embedded.changesets;
|
||||
const changesetIds = changesets.map(c => c.id);
|
||||
const key = action.itemId;
|
||||
|
||||
if (!key) {
|
||||
return state;
|
||||
}
|
||||
|
||||
let oldByIds = {};
|
||||
if (state[key] && state[key].byId) {
|
||||
oldByIds = state[key].byId;
|
||||
}
|
||||
|
||||
const byIds = extractChangesetsByIds(changesets);
|
||||
|
||||
return {
|
||||
...state,
|
||||
[key]: {
|
||||
byId: {
|
||||
...oldByIds,
|
||||
...byIds
|
||||
},
|
||||
list: {
|
||||
entries: changesetIds,
|
||||
entry: {
|
||||
page: payload.page,
|
||||
pageTotal: payload.pageTotal,
|
||||
_links: payload._links
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
function extractChangesetsByIds(changesets: any) {
|
||||
const changesetsByIds = {};
|
||||
|
||||
for (let changeset of changesets) {
|
||||
changesetsByIds[changeset.id] = changeset;
|
||||
}
|
||||
|
||||
return changesetsByIds;
|
||||
}
|
||||
|
||||
//selectors
|
||||
export function getChangesets(
|
||||
state: Object,
|
||||
repository: Repository,
|
||||
branch?: Branch
|
||||
) {
|
||||
const key = createItemId(repository, branch);
|
||||
|
||||
const changesets = state.changesets[key];
|
||||
if (!changesets) {
|
||||
return null;
|
||||
}
|
||||
return changesets.list.entries.map((id: string) => {
|
||||
return changesets.byId[id];
|
||||
});
|
||||
}
|
||||
|
||||
export function isFetchChangesetsPending(
|
||||
state: Object,
|
||||
repository: Repository,
|
||||
branch?: Branch
|
||||
) {
|
||||
return isPending(state, FETCH_CHANGESETS, createItemId(repository, branch));
|
||||
}
|
||||
|
||||
export function getFetchChangesetsFailure(
|
||||
state: Object,
|
||||
repository: Repository,
|
||||
branch?: Branch
|
||||
) {
|
||||
return getFailure(state, FETCH_CHANGESETS, createItemId(repository, branch));
|
||||
}
|
||||
|
||||
const selectList = (state: Object, repository: Repository, branch?: Branch) => {
|
||||
const itemId = createItemId(repository, branch);
|
||||
if (state.changesets[itemId] && state.changesets[itemId].list) {
|
||||
return state.changesets[itemId].list;
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
const selectListEntry = (
|
||||
state: Object,
|
||||
repository: Repository,
|
||||
branch?: Branch
|
||||
): Object => {
|
||||
const list = selectList(state, repository, branch);
|
||||
if (list.entry) {
|
||||
return list.entry;
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
export const selectListAsCollection = (
|
||||
state: Object,
|
||||
repository: Repository,
|
||||
branch?: Branch
|
||||
): PagedCollection => {
|
||||
return selectListEntry(state, repository, branch);
|
||||
};
|
||||
307
scm-ui/src/repos/modules/changesets.test.js
Normal file
307
scm-ui/src/repos/modules/changesets.test.js
Normal file
@@ -0,0 +1,307 @@
|
||||
// @flow
|
||||
|
||||
import configureMockStore from "redux-mock-store";
|
||||
import thunk from "redux-thunk";
|
||||
import fetchMock from "fetch-mock";
|
||||
import reducer, {
|
||||
FETCH_CHANGESETS,
|
||||
FETCH_CHANGESETS_FAILURE,
|
||||
FETCH_CHANGESETS_PENDING,
|
||||
FETCH_CHANGESETS_SUCCESS,
|
||||
fetchChangesets,
|
||||
fetchChangesetsSuccess,
|
||||
getChangesets,
|
||||
getFetchChangesetsFailure,
|
||||
isFetchChangesetsPending
|
||||
} from "./changesets";
|
||||
|
||||
const branch = {
|
||||
name: "specific",
|
||||
revision: "123",
|
||||
_links: {
|
||||
history: {
|
||||
href:
|
||||
"http://scm/api/rest/v2/repositories/foo/bar/branches/specific/changesets"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const repository = {
|
||||
namespace: "foo",
|
||||
name: "bar",
|
||||
type: "GIT",
|
||||
_links: {
|
||||
self: {
|
||||
href: "http://scm/api/rest/v2/repositories/foo/bar"
|
||||
},
|
||||
changesets: {
|
||||
href: "http://scm/api/rest/v2/repositories/foo/bar/changesets"
|
||||
},
|
||||
branches: {
|
||||
href:
|
||||
"http://scm/api/rest/v2/repositories/foo/bar/branches/specific/branches"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const changesets = {};
|
||||
|
||||
describe("changesets", () => {
|
||||
describe("fetching of changesets", () => {
|
||||
const DEFAULT_BRANCH_URL =
|
||||
"http://scm/api/rest/v2/repositories/foo/bar/changesets";
|
||||
const SPECIFIC_BRANCH_URL =
|
||||
"http://scm/api/rest/v2/repositories/foo/bar/branches/specific/changesets";
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
|
||||
afterEach(() => {
|
||||
fetchMock.reset();
|
||||
fetchMock.restore();
|
||||
});
|
||||
|
||||
it("should fetch changesets for default branch", () => {
|
||||
fetchMock.getOnce(DEFAULT_BRANCH_URL, "{}");
|
||||
|
||||
const expectedActions = [
|
||||
{
|
||||
type: FETCH_CHANGESETS_PENDING,
|
||||
itemId: "foo/bar"
|
||||
},
|
||||
{
|
||||
type: FETCH_CHANGESETS_SUCCESS,
|
||||
payload: changesets,
|
||||
itemId: "foo/bar"
|
||||
}
|
||||
];
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchChangesets(repository)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
it("should fetch changesets for specific branch", () => {
|
||||
const itemId = "foo/bar/specific";
|
||||
fetchMock.getOnce(SPECIFIC_BRANCH_URL, "{}");
|
||||
|
||||
const expectedActions = [
|
||||
{
|
||||
type: FETCH_CHANGESETS_PENDING,
|
||||
itemId
|
||||
},
|
||||
{
|
||||
type: FETCH_CHANGESETS_SUCCESS,
|
||||
payload: changesets,
|
||||
itemId
|
||||
}
|
||||
];
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchChangesets(repository, branch)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail fetching changesets on error", () => {
|
||||
const itemId = "foo/bar";
|
||||
fetchMock.getOnce(DEFAULT_BRANCH_URL, 500);
|
||||
|
||||
const expectedActions = [
|
||||
{
|
||||
type: FETCH_CHANGESETS_PENDING,
|
||||
itemId
|
||||
}
|
||||
];
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchChangesets(repository)).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", () => {
|
||||
const itemId = "foo/bar/specific";
|
||||
fetchMock.getOnce(SPECIFIC_BRANCH_URL, 500);
|
||||
|
||||
const expectedActions = [
|
||||
{
|
||||
type: FETCH_CHANGESETS_PENDING,
|
||||
itemId
|
||||
}
|
||||
];
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchChangesets(repository, branch)).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 fetch changesets by page", () => {
|
||||
fetchMock.getOnce(DEFAULT_BRANCH_URL + "?page=4", "{}");
|
||||
|
||||
const expectedActions = [
|
||||
{
|
||||
type: FETCH_CHANGESETS_PENDING,
|
||||
itemId: "foo/bar"
|
||||
},
|
||||
{
|
||||
type: FETCH_CHANGESETS_SUCCESS,
|
||||
payload: changesets,
|
||||
itemId: "foo/bar"
|
||||
}
|
||||
];
|
||||
|
||||
const store = mockStore({});
|
||||
return store
|
||||
.dispatch(fetchChangesets(repository, undefined, 5))
|
||||
.then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
it("should fetch changesets by branch and page", () => {
|
||||
fetchMock.getOnce(SPECIFIC_BRANCH_URL + "?page=4", "{}");
|
||||
|
||||
const expectedActions = [
|
||||
{
|
||||
type: FETCH_CHANGESETS_PENDING,
|
||||
itemId: "foo/bar/specific"
|
||||
},
|
||||
{
|
||||
type: FETCH_CHANGESETS_SUCCESS,
|
||||
payload: changesets,
|
||||
itemId: "foo/bar/specific"
|
||||
}
|
||||
];
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchChangesets(repository, branch, 5)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("changesets reducer", () => {
|
||||
const responseBody = {
|
||||
page: 1,
|
||||
pageTotal: 10,
|
||||
_links: {},
|
||||
_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 set state to received changesets", () => {
|
||||
const newState = reducer(
|
||||
{},
|
||||
fetchChangesetsSuccess(repository, undefined, responseBody)
|
||||
);
|
||||
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");
|
||||
expect(newState["foo/bar"].list).toEqual({
|
||||
entry: {
|
||||
page: 1,
|
||||
pageTotal: 10,
|
||||
_links: {}
|
||||
},
|
||||
entries: ["changeset1", "changeset2", "changeset3"]
|
||||
});
|
||||
});
|
||||
|
||||
it("should not remove existing changesets", () => {
|
||||
const state = {
|
||||
"foo/bar": {
|
||||
byId: {
|
||||
id2: { id: "id2" },
|
||||
id1: { id: "id1" }
|
||||
},
|
||||
list: {
|
||||
entries: ["id1", "id2"]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const newState = reducer(
|
||||
state,
|
||||
fetchChangesetsSuccess(repository, undefined, responseBody)
|
||||
);
|
||||
|
||||
const fooBar = newState["foo/bar"];
|
||||
|
||||
expect(fooBar.list.entries).toEqual([
|
||||
"changeset1",
|
||||
"changeset2",
|
||||
"changeset3"
|
||||
]);
|
||||
expect(fooBar.byId["id2"]).toEqual({ id: "id2" });
|
||||
expect(fooBar.byId["id1"]).toEqual({ id: "id1" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("changeset selectors", () => {
|
||||
const error = new Error("Something went wrong");
|
||||
|
||||
it("should get all changesets for a given repository", () => {
|
||||
const state = {
|
||||
changesets: {
|
||||
"foo/bar": {
|
||||
byId: {
|
||||
id2: { id: "id2" },
|
||||
id1: { id: "id1" }
|
||||
},
|
||||
list: {
|
||||
entries: ["id1", "id2"]
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const result = getChangesets(state, repository);
|
||||
expect(result).toEqual([{ id: "id1" }, { id: "id2" }]);
|
||||
});
|
||||
|
||||
it("should return true, when fetching changesets is pending", () => {
|
||||
const state = {
|
||||
pending: {
|
||||
[FETCH_CHANGESETS + "/foo/bar"]: true
|
||||
}
|
||||
};
|
||||
|
||||
expect(isFetchChangesetsPending(state, repository)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should return false, when fetching changesets is not pending", () => {
|
||||
expect(isFetchChangesetsPending({}, repository)).toEqual(false);
|
||||
});
|
||||
|
||||
it("should return error if fetching changesets failed", () => {
|
||||
const state = {
|
||||
failure: {
|
||||
[FETCH_CHANGESETS + "/foo/bar"]: error
|
||||
}
|
||||
};
|
||||
|
||||
expect(getFetchChangesetsFailure(state, repository)).toEqual(error);
|
||||
});
|
||||
|
||||
it("should return false if fetching changesets did not fail", () => {
|
||||
expect(getFetchChangesetsFailure({}, repository)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -402,7 +402,7 @@ describe("repos fetch", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should disapatch failure if server returns status code 500", () => {
|
||||
it("should dispatch failure if server returns status code 500", () => {
|
||||
fetchMock.postOnce(REPOS_URL, {
|
||||
status: 500
|
||||
});
|
||||
|
||||
@@ -14,7 +14,7 @@ type Props = {
|
||||
|
||||
class TypeSelector extends React.Component<Props> {
|
||||
render() {
|
||||
const { t, type, handleTypeChange, loading, label, helpText } = this.props;
|
||||
const { type, handleTypeChange, loading, label, helpText } = this.props;
|
||||
const types = ["READ", "OWNER", "WRITE"];
|
||||
|
||||
return (
|
||||
|
||||
@@ -5,12 +5,15 @@ import "../../../../tests/i18n";
|
||||
import DeletePermissionButton from "./DeletePermissionButton";
|
||||
|
||||
import { confirmAlert } from "@scm-manager/ui-components";
|
||||
import ReactRouterEnzymeContext from "react-router-enzyme-context";
|
||||
jest.mock("@scm-manager/ui-components", () => ({
|
||||
confirmAlert: jest.fn(),
|
||||
DeleteButton: require.requireActual("@scm-manager/ui-components").DeleteButton
|
||||
}));
|
||||
|
||||
describe("DeletePermissionButton", () => {
|
||||
const options = new ReactRouterEnzymeContext();
|
||||
|
||||
it("should render nothing, if the delete link is missing", () => {
|
||||
const permission = {
|
||||
_links: {}
|
||||
@@ -20,7 +23,8 @@ describe("DeletePermissionButton", () => {
|
||||
<DeletePermissionButton
|
||||
permission={permission}
|
||||
deletePermission={() => {}}
|
||||
/>
|
||||
/>,
|
||||
options.get()
|
||||
);
|
||||
expect(navLink.text()).toBe("");
|
||||
});
|
||||
@@ -38,7 +42,8 @@ describe("DeletePermissionButton", () => {
|
||||
<DeletePermissionButton
|
||||
permission={permission}
|
||||
deletePermission={() => {}}
|
||||
/>
|
||||
/>,
|
||||
options.get()
|
||||
);
|
||||
expect(navLink.text()).not.toBe("");
|
||||
});
|
||||
@@ -56,7 +61,8 @@ describe("DeletePermissionButton", () => {
|
||||
<DeletePermissionButton
|
||||
permission={permission}
|
||||
deletePermission={() => {}}
|
||||
/>
|
||||
/>,
|
||||
options.get()
|
||||
);
|
||||
button.find("button").simulate("click");
|
||||
|
||||
@@ -82,7 +88,8 @@ describe("DeletePermissionButton", () => {
|
||||
permission={permission}
|
||||
confirmDialog={false}
|
||||
deletePermission={capture}
|
||||
/>
|
||||
/>,
|
||||
options.get()
|
||||
);
|
||||
button.find("button").simulate("click");
|
||||
|
||||
|
||||
@@ -126,6 +126,7 @@ class Permissions extends React.Component<Props> {
|
||||
{t("permission.group-permission")}
|
||||
</th>
|
||||
<th>{t("permission.type")}</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
Reference in New Issue
Block a user