mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-15 09:46:16 +01:00
merge
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import sonia.scm.plugin.ExtensionPoint;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Optional;
|
||||
|
||||
@ExtensionPoint(multi = false)
|
||||
@FunctionalInterface
|
||||
public interface LogoutRedirection {
|
||||
Optional<URI> afterLogoutRedirectTo();
|
||||
}
|
||||
@@ -28,7 +28,7 @@ public abstract class GitRepositoryConfigMapper {
|
||||
@AfterMapping
|
||||
void appendLinks(@MappingTarget GitRepositoryConfigDto target, @Context Repository repository) {
|
||||
Links.Builder linksBuilder = linkingTo().self(self());
|
||||
if (RepositoryPermissions.modify(repository).isPermitted()) {
|
||||
if (RepositoryPermissions.custom("git", repository).isPermitted()) {
|
||||
linksBuilder.single(link("update", update()));
|
||||
}
|
||||
target.add(linksBuilder.build());
|
||||
|
||||
@@ -70,7 +70,7 @@ public class GitRepositoryConfigResource {
|
||||
})
|
||||
public Response setRepositoryConfig(@PathParam("namespace") String namespace, @PathParam("name") String name, GitRepositoryConfigDto dto) {
|
||||
Repository repository = getRepository(namespace, name);
|
||||
RepositoryPermissions.modify(repository).check();
|
||||
RepositoryPermissions.custom("git", repository).check();
|
||||
ConfigurationStore<GitRepositoryConfig> repositoryConfigStore = getStore(repository);
|
||||
GitRepositoryConfig config = repositoryConfigMapper.map(dto);
|
||||
repositoryConfigStore.set(config);
|
||||
|
||||
@@ -8,11 +8,10 @@ type Props = {
|
||||
repository: Repository,
|
||||
|
||||
// context props
|
||||
t: (string) => string
|
||||
t: string => string
|
||||
};
|
||||
|
||||
class CloneInformation extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
const { url, repository, t } = this.props;
|
||||
|
||||
@@ -51,7 +50,6 @@ class CloneInformation extends React.Component<Props> {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default translate("plugins")(CloneInformation);
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import type { Branch } from "@scm-manager/ui-types";
|
||||
import { translate } from "react-i18next";
|
||||
|
||||
type Props = {
|
||||
branch: Branch,
|
||||
t: string => string
|
||||
};
|
||||
|
||||
class GitBranchInformation extends React.Component<Props> {
|
||||
render() {
|
||||
const { branch, t } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h4>{t("scm-git-plugin.information.fetch")}</h4>
|
||||
<pre>
|
||||
<code>git fetch</code>
|
||||
</pre>
|
||||
<h4>{t("scm-git-plugin.information.checkout")}</h4>
|
||||
<pre>
|
||||
<code>git checkout {branch.name}</code>
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate("plugins")(GitBranchInformation);
|
||||
@@ -10,7 +10,7 @@ type Configuration = {
|
||||
gcExpression?: string,
|
||||
nonFastForwardDisallowed: boolean,
|
||||
_links: Links
|
||||
}
|
||||
};
|
||||
|
||||
type Props = {
|
||||
initialConfiguration: Configuration,
|
||||
@@ -19,25 +19,24 @@ type Props = {
|
||||
onConfigurationChange: (Configuration, boolean) => void,
|
||||
|
||||
// context props
|
||||
t: (string) => string
|
||||
}
|
||||
t: string => string
|
||||
};
|
||||
|
||||
type State = Configuration & {
|
||||
|
||||
}
|
||||
type State = Configuration & {};
|
||||
|
||||
class GitConfigurationForm extends React.Component<Props, State> {
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = { ...props.initialConfiguration };
|
||||
}
|
||||
|
||||
|
||||
handleChange = (value: any, name: string) => {
|
||||
this.setState({
|
||||
[name]: value
|
||||
}, () => this.props.onConfigurationChange(this.state, true));
|
||||
this.setState(
|
||||
{
|
||||
[name]: value
|
||||
},
|
||||
() => this.props.onConfigurationChange(this.state, true)
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
@@ -46,24 +45,25 @@ class GitConfigurationForm extends React.Component<Props, State> {
|
||||
|
||||
return (
|
||||
<>
|
||||
<InputField name="gcExpression"
|
||||
label={t("scm-git-plugin.config.gcExpression")}
|
||||
helpText={t("scm-git-plugin.config.gcExpressionHelpText")}
|
||||
value={gcExpression}
|
||||
onChange={this.handleChange}
|
||||
disabled={readOnly}
|
||||
<InputField
|
||||
name="gcExpression"
|
||||
label={t("scm-git-plugin.config.gcExpression")}
|
||||
helpText={t("scm-git-plugin.config.gcExpressionHelpText")}
|
||||
value={gcExpression}
|
||||
onChange={this.handleChange}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
<Checkbox name="nonFastForwardDisallowed"
|
||||
label={t("scm-git-plugin.config.nonFastForwardDisallowed")}
|
||||
helpText={t("scm-git-plugin.config.nonFastForwardDisallowedHelpText")}
|
||||
checked={nonFastForwardDisallowed}
|
||||
onChange={this.handleChange}
|
||||
disabled={readOnly}
|
||||
<Checkbox
|
||||
name="nonFastForwardDisallowed"
|
||||
label={t("scm-git-plugin.config.nonFastForwardDisallowed")}
|
||||
helpText={t("scm-git-plugin.config.nonFastForwardDisallowedHelpText")}
|
||||
checked={nonFastForwardDisallowed}
|
||||
onChange={this.handleChange}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default translate("plugins")(GitConfigurationForm);
|
||||
|
||||
@@ -121,6 +121,7 @@ class RepositoryConfig extends React.Component<Props, State> {
|
||||
if (!(loadingBranches || loadingDefaultBranch)) {
|
||||
return (
|
||||
<>
|
||||
<hr />
|
||||
<Subtitle subtitle={t("scm-git-plugin.repo-config.title")}/>
|
||||
{this.renderBranchChangedNotification()}
|
||||
<form onSubmit={this.submit}>
|
||||
@@ -133,7 +134,6 @@ class RepositoryConfig extends React.Component<Props, State> {
|
||||
/>
|
||||
{ submitButton }
|
||||
</form>
|
||||
<hr />
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
|
||||
@@ -6,6 +6,7 @@ import GitAvatar from "./GitAvatar";
|
||||
|
||||
import {ConfigurationBinder as cfgBinder} from "@scm-manager/ui-components";
|
||||
import GitGlobalConfiguration from "./GitGlobalConfiguration";
|
||||
import GitBranchInformation from "./GitBranchInformation";
|
||||
import GitMergeInformation from "./GitMergeInformation";
|
||||
import RepositoryConfig from "./RepositoryConfig";
|
||||
|
||||
@@ -20,6 +21,11 @@ binder.bind(
|
||||
ProtocolInformation,
|
||||
gitPredicate
|
||||
);
|
||||
binder.bind(
|
||||
"repos.branch-details.information",
|
||||
GitBranchInformation,
|
||||
gitPredicate
|
||||
);
|
||||
binder.bind(
|
||||
"repos.repository-merge.information",
|
||||
GitMergeInformation,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
{
|
||||
"scm-git-plugin": {
|
||||
"information": {
|
||||
"clone" : "Repository klonen",
|
||||
"create" : "Neues Repository erstellen",
|
||||
"replace" : "Ein bestehendes Repository aktualisieren",
|
||||
"clone": "Repository klonen",
|
||||
"create": "Neues Repository erstellen",
|
||||
"replace": "Ein bestehendes Repository aktualisieren",
|
||||
"fetch": "Remote-Änderungen herunterladen",
|
||||
"checkout": "Branch wechseln",
|
||||
"merge": {
|
||||
"heading": "Merge des Source Branch in den Target Branch",
|
||||
"checkout": "1. Sicherstellen, dass der Workspace aufgeräumt ist und der Target Branch ausgecheckt wurde.",
|
||||
@@ -37,7 +39,7 @@
|
||||
"success": "Der standard Branch wurde geändert!"
|
||||
}
|
||||
},
|
||||
"permissions" : {
|
||||
"permissions": {
|
||||
"configuration": {
|
||||
"read,write": {
|
||||
"git": {
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
"clone": "Clone the repository",
|
||||
"create": "Create a new repository",
|
||||
"replace": "Push an existing repository",
|
||||
"fetch": "Get remote changes",
|
||||
"checkout": "Switch branch",
|
||||
"merge": {
|
||||
"heading": "How to merge source branch into target branch",
|
||||
"checkout": "1. Make sure your workspace is clean and checkout target branch",
|
||||
|
||||
@@ -10,4 +10,4 @@ writer = configuration:write:git
|
||||
readerWriter = configuration:*:git,repository:*:id
|
||||
admin = *
|
||||
repoRead = repository:read:*
|
||||
repoWrite = repository:modify:*
|
||||
repoWrite = repository:modify:*,repository:git:*
|
||||
|
||||
30
scm-plugins/scm-hg-plugin/src/main/js/HgBranchInformation.js
Normal file
30
scm-plugins/scm-hg-plugin/src/main/js/HgBranchInformation.js
Normal file
@@ -0,0 +1,30 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import type { Branch } from "@scm-manager/ui-types";
|
||||
import { translate } from "react-i18next";
|
||||
|
||||
type Props = {
|
||||
branch: Branch,
|
||||
t: string => string
|
||||
};
|
||||
|
||||
class HgBranchInformation extends React.Component<Props> {
|
||||
render() {
|
||||
const { branch, t } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h4>{t("scm-hg-plugin.information.fetch")}</h4>
|
||||
<pre>
|
||||
<code>hg pull</code>
|
||||
</pre>
|
||||
<h4>{t("scm-hg-plugin.information.checkout")}</h4>
|
||||
<pre>
|
||||
<code>hg update {branch.name}</code>
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate("plugins")(HgBranchInformation);
|
||||
@@ -4,14 +4,29 @@ import ProtocolInformation from "./ProtocolInformation";
|
||||
import HgAvatar from "./HgAvatar";
|
||||
import { ConfigurationBinder as cfgBinder } from "@scm-manager/ui-components";
|
||||
import HgGlobalConfiguration from "./HgGlobalConfiguration";
|
||||
import HgBranchInformation from "./HgBranchInformation";
|
||||
|
||||
const hgPredicate = (props: Object) => {
|
||||
return props.repository && props.repository.type === "hg";
|
||||
};
|
||||
|
||||
binder.bind("repos.repository-details.information", ProtocolInformation, hgPredicate);
|
||||
binder.bind(
|
||||
"repos.repository-details.information",
|
||||
ProtocolInformation,
|
||||
hgPredicate
|
||||
);
|
||||
binder.bind(
|
||||
"repos.branch-details.information",
|
||||
HgBranchInformation,
|
||||
hgPredicate
|
||||
);
|
||||
binder.bind("repos.repository-avatar", HgAvatar, hgPredicate);
|
||||
|
||||
// bind global configuration
|
||||
|
||||
cfgBinder.bindGlobal("/hg", "scm-hg-plugin.config.link", "hgConfig", HgGlobalConfiguration);
|
||||
cfgBinder.bindGlobal(
|
||||
"/hg",
|
||||
"scm-hg-plugin.config.link",
|
||||
"hgConfig",
|
||||
HgGlobalConfiguration
|
||||
);
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
"information": {
|
||||
"clone" : "Repository klonen",
|
||||
"create" : "Neues Repository erstellen",
|
||||
"replace" : "Ein bestehendes Repository aktualisieren"
|
||||
"replace" : "Ein bestehendes Repository aktualisieren",
|
||||
"fetch": "Remote-Änderungen herunterladen",
|
||||
"checkout": "Branch wechseln"
|
||||
},
|
||||
"config": {
|
||||
"link": "Mercurial",
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
"information": {
|
||||
"clone" : "Clone the repository",
|
||||
"create" : "Create a new repository",
|
||||
"replace" : "Push an existing repository"
|
||||
"replace" : "Push an existing repository",
|
||||
"fetch": "Get remote changes",
|
||||
"checkout": "Switch branch"
|
||||
},
|
||||
"config": {
|
||||
"link": "Mercurial",
|
||||
|
||||
@@ -30,9 +30,9 @@
|
||||
"@scm-manager/ui-types": "2.0.0-SNAPSHOT",
|
||||
"classnames": "^2.2.6",
|
||||
"moment": "^2.22.2",
|
||||
"react": "^16.5.2",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6",
|
||||
"react-diff-view": "^1.8.1",
|
||||
"react-dom": "^16.5.2",
|
||||
"react-i18next": "^7.11.0",
|
||||
"react-jss": "^8.6.1",
|
||||
"react-router-dom": "^4.3.1",
|
||||
@@ -63,4 +63,4 @@
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ class MailLink extends React.Component<Props> {
|
||||
if (!address) {
|
||||
return null;
|
||||
}
|
||||
return <a href={"mailto: " + address}>{address}</a>;
|
||||
return <a href={"mailto:" + address}>{address}</a>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,8 @@ type Props = {
|
||||
value?: string,
|
||||
onChange: (value: string, name?: string) => void,
|
||||
loading?: boolean,
|
||||
helpText?: string
|
||||
helpText?: string,
|
||||
disabled?: boolean
|
||||
};
|
||||
|
||||
class Select extends React.Component<Props> {
|
||||
@@ -34,7 +35,7 @@ class Select extends React.Component<Props> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { options, value, label, helpText, loading } = this.props;
|
||||
const { options, value, label, helpText, loading, disabled } = this.props;
|
||||
const loadingClass = loading ? "is-loading" : "";
|
||||
|
||||
|
||||
@@ -51,6 +52,7 @@ class Select extends React.Component<Props> {
|
||||
}}
|
||||
value={value}
|
||||
onChange={this.handleInput}
|
||||
disabled={disabled}
|
||||
>
|
||||
{options.map(opt => {
|
||||
return (
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import injectSheets from "react-jss";
|
||||
import classNames from "classnames";
|
||||
import { translate } from "react-i18next";
|
||||
import {Button} from "../buttons";
|
||||
|
||||
const styles = {
|
||||
panel: {
|
||||
@@ -39,14 +40,16 @@ type Props = DiffObjectProps & {
|
||||
};
|
||||
|
||||
type State = {
|
||||
collapsed: boolean
|
||||
collapsed: boolean,
|
||||
sideBySide: boolean
|
||||
};
|
||||
|
||||
class DiffFile extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
collapsed: false
|
||||
collapsed: false,
|
||||
sideBySide: false
|
||||
};
|
||||
}
|
||||
|
||||
@@ -56,6 +59,12 @@ class DiffFile extends React.Component<Props, State> {
|
||||
}));
|
||||
};
|
||||
|
||||
toggleSideBySide = () => {
|
||||
this.setState(state => ({
|
||||
sideBySide: !state.sideBySide
|
||||
}));
|
||||
};
|
||||
|
||||
setCollapse = (collapsed: boolean) => {
|
||||
this.setState({
|
||||
collapsed
|
||||
@@ -149,10 +158,10 @@ class DiffFile extends React.Component<Props, State> {
|
||||
file,
|
||||
fileControlFactory,
|
||||
fileAnnotationFactory,
|
||||
sideBySide,
|
||||
classes
|
||||
classes,
|
||||
t
|
||||
} = this.props;
|
||||
const { collapsed } = this.state;
|
||||
const { collapsed, sideBySide } = this.state;
|
||||
const viewType = sideBySide ? "split" : "unified";
|
||||
|
||||
let body = null;
|
||||
@@ -173,14 +182,10 @@ class DiffFile extends React.Component<Props, State> {
|
||||
}
|
||||
|
||||
const fileControls = fileControlFactory ? fileControlFactory(file, this.setCollapse) : null;
|
||||
return (
|
||||
<div className={classNames("panel", classes.panel)}>
|
||||
return <div className={classNames("panel", classes.panel)}>
|
||||
<div className="panel-heading">
|
||||
<div className="level">
|
||||
<div
|
||||
className={classNames("level-left", classes.titleHeader)}
|
||||
onClick={this.toggleCollapse}
|
||||
>
|
||||
<div className={classNames("level-left", classes.titleHeader)} onClick={this.toggleCollapse}>
|
||||
<i className={icon} />
|
||||
<span className={classes.title}>
|
||||
{this.renderFileTitle(file)}
|
||||
@@ -189,12 +194,21 @@ class DiffFile extends React.Component<Props, State> {
|
||||
{this.renderChangeTag(file)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="level-right">{fileControls}</div>
|
||||
<div className="level-right">
|
||||
<Button action={this.toggleSideBySide}>
|
||||
<span className="icon is-small">
|
||||
<i className={classNames("fas", sideBySide ? "fa-align-left" : "fa-columns")} />
|
||||
</span>
|
||||
<span className="is-hidden-mobile">
|
||||
{t(sideBySide ? "diff.combined" : "diff.sideBySide")}
|
||||
</span>
|
||||
</Button>
|
||||
{fileControls}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{body}
|
||||
</div>
|
||||
);
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
import React from "react";
|
||||
import type { Changeset } from "@scm-manager/ui-types";
|
||||
import { ExtensionPoint } from "@scm-manager/ui-extensions";
|
||||
import {translate} from "react-i18next";
|
||||
import { translate } from "react-i18next";
|
||||
|
||||
type Props = {
|
||||
changeset: Changeset,
|
||||
|
||||
// context props
|
||||
t: (string) => string
|
||||
t: string => string
|
||||
};
|
||||
|
||||
class ChangesetAuthor extends React.Component<Props> {
|
||||
@@ -28,7 +28,10 @@ class ChangesetAuthor extends React.Component<Props> {
|
||||
renderWithMail(name: string, mail: string) {
|
||||
const { t } = this.props;
|
||||
return (
|
||||
<a href={"mailto: " + mail} title={t("changeset.author.mailto") + " " + mail}>
|
||||
<a
|
||||
href={"mailto:" + mail}
|
||||
title={t("changeset.author.mailto") + " " + mail}
|
||||
>
|
||||
{name}
|
||||
</a>
|
||||
);
|
||||
@@ -44,7 +47,7 @@ class ChangesetAuthor extends React.Component<Props> {
|
||||
props={{ changeset: this.props.changeset }}
|
||||
renderAll={true}
|
||||
/>
|
||||
</>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,11 +11,10 @@ type Props = {
|
||||
changeset: Changeset,
|
||||
|
||||
// context props
|
||||
t: (string) => string
|
||||
}
|
||||
t: string => string
|
||||
};
|
||||
|
||||
class ChangesetButtonGroup extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
const { repository, changeset, t } = this.props;
|
||||
|
||||
@@ -26,7 +25,7 @@ class ChangesetButtonGroup extends React.Component<Props> {
|
||||
<ButtonGroup className="is-pulled-right">
|
||||
<Button link={changesetLink}>
|
||||
<span className="icon">
|
||||
<i className="fas fa-code-branch"></i>
|
||||
<i className="fas fa-exchange-alt" />
|
||||
</span>
|
||||
<span className="is-hidden-mobile is-hidden-tablet-only">
|
||||
{t("changeset.buttons.details")}
|
||||
@@ -34,7 +33,7 @@ class ChangesetButtonGroup extends React.Component<Props> {
|
||||
</Button>
|
||||
<Button link={sourcesLink}>
|
||||
<span className="icon">
|
||||
<i className="fas fa-code"></i>
|
||||
<i className="fas fa-code" />
|
||||
</span>
|
||||
<span className="is-hidden-mobile is-hidden-tablet-only">
|
||||
{t("changeset.buttons.sources")}
|
||||
@@ -43,7 +42,6 @@ class ChangesetButtonGroup extends React.Component<Props> {
|
||||
</ButtonGroup>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default translate("repos")(ChangesetButtonGroup);
|
||||
|
||||
@@ -6623,14 +6623,14 @@ react-dom@^16.4.2:
|
||||
prop-types "^15.6.2"
|
||||
scheduler "^0.10.0"
|
||||
|
||||
react-dom@^16.5.2:
|
||||
version "16.5.2"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.5.2.tgz#b69ee47aa20bab5327b2b9d7c1fe2a30f2cfa9d7"
|
||||
react-dom@^16.8.6:
|
||||
version "16.8.6"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.6.tgz#71d6303f631e8b0097f56165ef608f051ff6e10f"
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
object-assign "^4.1.1"
|
||||
prop-types "^15.6.2"
|
||||
schedule "^0.5.0"
|
||||
scheduler "^0.13.6"
|
||||
|
||||
react-i18next@^7.11.0:
|
||||
version "7.13.0"
|
||||
@@ -6755,14 +6755,14 @@ react@^16.4.2:
|
||||
prop-types "^15.6.2"
|
||||
scheduler "^0.10.0"
|
||||
|
||||
react@^16.5.2:
|
||||
version "16.5.2"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-16.5.2.tgz#19f6b444ed139baa45609eee6dc3d318b3895d42"
|
||||
react@^16.8.6:
|
||||
version "16.8.6"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-16.8.6.tgz#ad6c3a9614fd3a4e9ef51117f54d888da01f2bbe"
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
object-assign "^4.1.1"
|
||||
prop-types "^15.6.2"
|
||||
schedule "^0.5.0"
|
||||
scheduler "^0.13.6"
|
||||
|
||||
read-only-stream@^2.0.0:
|
||||
version "2.0.0"
|
||||
@@ -7229,6 +7229,13 @@ scheduler@^0.10.0:
|
||||
loose-envify "^1.1.0"
|
||||
object-assign "^4.1.1"
|
||||
|
||||
scheduler@^0.13.6:
|
||||
version "0.13.6"
|
||||
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.13.6.tgz#466a4ec332467b31a91b9bf74e5347072e4cd889"
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
object-assign "^4.1.1"
|
||||
|
||||
select@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d"
|
||||
|
||||
@@ -4,5 +4,11 @@ import type {Links} from "./hal";
|
||||
export type Branch = {
|
||||
name: string,
|
||||
revision: string,
|
||||
defaultBranch?: boolean,
|
||||
_links: Links
|
||||
}
|
||||
|
||||
export type BranchRequest = {
|
||||
name: string,
|
||||
parent: string
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ export type { Group, Member } from "./Group";
|
||||
export type { Repository, RepositoryCollection, RepositoryGroup } from "./Repositories";
|
||||
export type { RepositoryType, RepositoryTypeCollection } from "./RepositoryTypes";
|
||||
|
||||
export type { Branch } from "./Branches";
|
||||
export type { Branch, BranchRequest } from "./Branches";
|
||||
|
||||
export type { Changeset } from "./Changesets";
|
||||
|
||||
|
||||
@@ -17,11 +17,12 @@
|
||||
"i18next-browser-languagedetector": "^2.2.2",
|
||||
"i18next-fetch-backend": "^0.1.0",
|
||||
"jss-nested": "^6.0.1",
|
||||
"memoize-one": "^5.0.4",
|
||||
"moment": "^2.22.2",
|
||||
"node-sass": "^4.9.3",
|
||||
"postcss-easy-import": "^3.0.0",
|
||||
"react": "^16.8.6",
|
||||
"query-string": "5",
|
||||
"react": "^16.8.6",
|
||||
"react-diff-view": "^1.8.1",
|
||||
"react-dom": "^16.8.6",
|
||||
"react-i18next": "^7.9.0",
|
||||
@@ -53,7 +54,7 @@
|
||||
"pre-commit": "jest && flow && eslint src"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scm-manager/ui-bundler": "^0.0.26",
|
||||
"@scm-manager/ui-bundler": "^0.0.27",
|
||||
"concat": "^1.0.3",
|
||||
"copyfiles": "^2.0.0",
|
||||
"enzyme": "^3.3.0",
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
},
|
||||
"groups": {
|
||||
"title": "Gruppen",
|
||||
"subtitle": "Verwaltung der Gruppen"
|
||||
"subtitle": "Verwaltung der Gruppen",
|
||||
"noGroups": "Keine Gruppen gefunden."
|
||||
},
|
||||
"singleGroup": {
|
||||
"errorTitle": "Fehler",
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
"menu": {
|
||||
"navigationLabel": "Repository Navigation",
|
||||
"informationNavLink": "Informationen",
|
||||
"branchesNavLink": "Branches",
|
||||
"historyNavLink": "Commits",
|
||||
"sourcesNavLink": "Sources",
|
||||
"settingsNavLink": "Einstellungen",
|
||||
@@ -36,15 +37,39 @@
|
||||
"overview": {
|
||||
"title": "Repositories",
|
||||
"subtitle": "Übersicht aller verfügbaren Repositories",
|
||||
"noRepositories": "Keine Repositories gefunden.",
|
||||
"createButton": "Repository erstellen"
|
||||
},
|
||||
"create": {
|
||||
"title": "Repository erstellen",
|
||||
"subtitle": "Erstellen eines neuen Repository"
|
||||
},
|
||||
"branches": {
|
||||
"overview": {
|
||||
"title": "Übersicht aller verfügbaren Branches",
|
||||
"noBranches": "Keine Branches gefunden.",
|
||||
"createButton": "Branch erstellen"
|
||||
},
|
||||
"table": {
|
||||
"branches": "Branches"
|
||||
},
|
||||
"create": {
|
||||
"title": "Branch erstellen",
|
||||
"source": "Quellbranch",
|
||||
"name": "Name",
|
||||
"submit": "Branch erstellen"
|
||||
}
|
||||
},
|
||||
"branch": {
|
||||
"name": "Name:",
|
||||
"commits": "Commits",
|
||||
"sources": "Sources",
|
||||
"defaultTag": "Default"
|
||||
},
|
||||
"changesets": {
|
||||
"errorTitle": "Fehler",
|
||||
"errorSubtitle": "Changesets konnten nicht abgerufen werden",
|
||||
"noChangesets": "Keine Changesets in diesem Branch gefunden.",
|
||||
"branchSelectorLabel": "Branches"
|
||||
},
|
||||
"changeset": {
|
||||
@@ -83,7 +108,8 @@
|
||||
"lastModified": "Zuletzt bearbeitet",
|
||||
"description": "Beschreibung",
|
||||
"size": "Größe"
|
||||
}
|
||||
},
|
||||
"noSources": "Keine Sources in diesem Branch gefunden."
|
||||
},
|
||||
"permission": {
|
||||
"title": "Berechtigungen bearbeiten",
|
||||
@@ -147,5 +173,9 @@
|
||||
"submit": "Ja",
|
||||
"cancel": "Nein"
|
||||
}
|
||||
},
|
||||
"diff": {
|
||||
"sideBySide": "Zweispalitg",
|
||||
"combined": "Kombiniert"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"users": {
|
||||
"title": "Benutzer",
|
||||
"subtitle": "Verwaltung der Benutzer",
|
||||
"noUsers": "Keine Benutzer gefunden.",
|
||||
"createButton": "Benutzer erstellen"
|
||||
},
|
||||
"singleUser": {
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
},
|
||||
"groups": {
|
||||
"title": "Groups",
|
||||
"subtitle": "Create, read, update and delete groups"
|
||||
"subtitle": "Create, read, update and delete groups",
|
||||
"noGroups": "No groups found."
|
||||
},
|
||||
"singleGroup": {
|
||||
"errorTitle": "Error",
|
||||
|
||||
@@ -11,7 +11,10 @@
|
||||
"validation": {
|
||||
"namespace-invalid": "The repository namespace is invalid",
|
||||
"name-invalid": "The repository name is invalid",
|
||||
"contact-invalid": "Contact must be a valid mail address"
|
||||
"contact-invalid": "Contact must be a valid mail address",
|
||||
"branch": {
|
||||
"nameInvalid": "The branch name is invalid"
|
||||
}
|
||||
},
|
||||
"help": {
|
||||
"namespaceHelpText": "The namespace of the repository. This name will be part of the repository url.",
|
||||
@@ -26,6 +29,7 @@
|
||||
"menu": {
|
||||
"navigationLabel": "Repository Navigation",
|
||||
"informationNavLink": "Information",
|
||||
"branchesNavLink": "Branches",
|
||||
"historyNavLink": "Commits",
|
||||
"sourcesNavLink": "Sources",
|
||||
"settingsNavLink": "Settings",
|
||||
@@ -36,15 +40,39 @@
|
||||
"overview": {
|
||||
"title": "Repositories",
|
||||
"subtitle": "Overview of available repositories",
|
||||
"noRepositories": "No repositories found.",
|
||||
"createButton": "Create Repository"
|
||||
},
|
||||
"create": {
|
||||
"title": "Create Repository",
|
||||
"subtitle": "Create a new repository"
|
||||
},
|
||||
"branches": {
|
||||
"overview": {
|
||||
"title": "Overview of all branches",
|
||||
"noBranches": "No branches found.",
|
||||
"createButton": "Create Branch"
|
||||
},
|
||||
"table": {
|
||||
"branches": "Branches"
|
||||
},
|
||||
"create": {
|
||||
"title": "Create Branch",
|
||||
"source": "Source Branch",
|
||||
"name": "Name",
|
||||
"submit": "Create Branch"
|
||||
}
|
||||
},
|
||||
"branch": {
|
||||
"name": "Name:",
|
||||
"commits": "Commits",
|
||||
"sources": "Sources",
|
||||
"defaultTag": "Default"
|
||||
},
|
||||
"changesets": {
|
||||
"errorTitle": "Error",
|
||||
"errorSubtitle": "Could not fetch changesets",
|
||||
"noChangesets": "No changesets found for this branch.",
|
||||
"branchSelectorLabel": "Branches"
|
||||
},
|
||||
"changeset": {
|
||||
@@ -83,7 +111,8 @@
|
||||
"lastModified": "Last modified",
|
||||
"description": "Description",
|
||||
"size": "Size"
|
||||
}
|
||||
},
|
||||
"noSources": "No sources found for this branch."
|
||||
},
|
||||
"permission": {
|
||||
"title": "Edit Permissions",
|
||||
@@ -155,6 +184,8 @@
|
||||
"modify": "modified",
|
||||
"rename": "renamed",
|
||||
"copy": "copied"
|
||||
}
|
||||
},
|
||||
"sideBySide": "side-by-side",
|
||||
"combined": "combined"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"users": {
|
||||
"title": "Users",
|
||||
"subtitle": "Create, read, update and delete users",
|
||||
"noUsers": "No users found.",
|
||||
"createButton": "Create User"
|
||||
},
|
||||
"singleUser": {
|
||||
|
||||
@@ -52,7 +52,7 @@ class Index extends Component<Props, State> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { indexResources, loading, error, t } = this.props;
|
||||
const { indexResources, loading, error } = this.props;
|
||||
const { pluginsLoaded } = this.state;
|
||||
|
||||
if (error) {
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
logout,
|
||||
isAuthenticated,
|
||||
isLogoutPending,
|
||||
getLogoutFailure
|
||||
getLogoutFailure, isRedirecting
|
||||
} from "../modules/auth";
|
||||
import { Loading, ErrorPage } from "@scm-manager/ui-components";
|
||||
import { getLogoutLink } from "../modules/indexResource";
|
||||
@@ -16,6 +16,7 @@ import { getLogoutLink } from "../modules/indexResource";
|
||||
type Props = {
|
||||
authenticated: boolean,
|
||||
loading: boolean,
|
||||
redirecting: boolean,
|
||||
error: Error,
|
||||
logoutLink: string,
|
||||
|
||||
@@ -32,7 +33,7 @@ class Logout extends React.Component<Props> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { authenticated, loading, error, t } = this.props;
|
||||
const { authenticated, redirecting, loading, error, t } = this.props;
|
||||
if (error) {
|
||||
return (
|
||||
<ErrorPage
|
||||
@@ -41,7 +42,7 @@ class Logout extends React.Component<Props> {
|
||||
error={error}
|
||||
/>
|
||||
);
|
||||
} else if (loading || authenticated) {
|
||||
} else if (loading || authenticated || redirecting) {
|
||||
return <Loading />;
|
||||
} else {
|
||||
return <Redirect to="/login" />;
|
||||
@@ -52,11 +53,13 @@ class Logout extends React.Component<Props> {
|
||||
const mapStateToProps = state => {
|
||||
const authenticated = isAuthenticated(state);
|
||||
const loading = isLogoutPending(state);
|
||||
const redirecting = isRedirecting(state);
|
||||
const error = getLogoutFailure(state);
|
||||
const logoutLink = getLogoutLink(state);
|
||||
return {
|
||||
authenticated,
|
||||
loading,
|
||||
redirecting,
|
||||
error,
|
||||
logoutLink
|
||||
};
|
||||
|
||||
@@ -61,13 +61,11 @@ class PluginLoader extends React.Component<Props, State> {
|
||||
}
|
||||
return promises.reduce((chain, current) => {
|
||||
return chain.then(chainResults => {
|
||||
return current.then(currentResult => [...chainResults, currentResult])
|
||||
}
|
||||
);
|
||||
return current.then(currentResult => [...chainResults, currentResult]);
|
||||
});
|
||||
}, Promise.resolve([]));
|
||||
};
|
||||
|
||||
|
||||
loadPlugin = (plugin: Plugin) => {
|
||||
this.setState({
|
||||
message: `loading ${plugin.name}`
|
||||
|
||||
@@ -19,7 +19,7 @@ import namespaceStrategies from "./config/modules/namespaceStrategies";
|
||||
import indexResources from "./modules/indexResource";
|
||||
|
||||
import type { BrowserHistory } from "history/createBrowserHistory";
|
||||
import branches from "./repos/modules/branches";
|
||||
import branches from "./repos/branches/modules/branches";
|
||||
|
||||
function createReduxStore(history: BrowserHistory) {
|
||||
const composeEnhancers =
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import type { Group } from "@scm-manager/ui-types";
|
||||
import { Checkbox } from "@scm-manager/ui-components"
|
||||
import { Checkbox } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = {
|
||||
group: Group
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
Page,
|
||||
PageActions,
|
||||
Button,
|
||||
Notification,
|
||||
LinkPaginator,
|
||||
getPageFromMatch
|
||||
} from "@scm-manager/ui-components";
|
||||
@@ -64,6 +65,13 @@ class Groups extends React.Component<Props, State> {
|
||||
this.setState({ page: page });
|
||||
}
|
||||
|
||||
onPageChange = (link: string) => {
|
||||
this.props.fetchGroupsByLink(link);
|
||||
};
|
||||
|
||||
/**
|
||||
* reflect page transitions in the uri
|
||||
*/
|
||||
componentDidUpdate = (prevProps: Props) => {
|
||||
const { list, page, location, fetchGroupsByPage, groupLink } = this.props;
|
||||
if (list && page) {
|
||||
@@ -89,14 +97,26 @@ class Groups extends React.Component<Props, State> {
|
||||
history.push("/groups/?q=" + filter);
|
||||
}}
|
||||
>
|
||||
<GroupTable groups={groups} />
|
||||
{this.renderPaginator()}
|
||||
{this.renderGroupTable()}
|
||||
{this.renderCreateButton()}
|
||||
{this.renderPageActionCreateButton()}
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
renderGroupTable() {
|
||||
const { groups, t } = this.props;
|
||||
if (groups && groups.length > 0) {
|
||||
return (
|
||||
<>
|
||||
<GroupTable groups={groups} />
|
||||
{this.renderPaginator()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return <Notification type="info">{t("groups.noGroups")}</Notification>;
|
||||
}
|
||||
|
||||
renderPaginator = () => {
|
||||
const { list, page } = this.props;
|
||||
if (list) {
|
||||
|
||||
@@ -29,6 +29,7 @@ export const LOGOUT = "scm/auth/LOGOUT";
|
||||
export const LOGOUT_PENDING = `${LOGOUT}_${types.PENDING_SUFFIX}`;
|
||||
export const LOGOUT_SUCCESS = `${LOGOUT}_${types.SUCCESS_SUFFIX}`;
|
||||
export const LOGOUT_FAILURE = `${LOGOUT}_${types.FAILURE_SUFFIX}`;
|
||||
export const LOGOUT_REDIRECT = `${LOGOUT}_REDIRECT`;
|
||||
|
||||
// Reducer
|
||||
|
||||
@@ -54,6 +55,13 @@ export default function reducer(
|
||||
case LOGOUT_SUCCESS:
|
||||
return initialState;
|
||||
|
||||
case LOGOUT_REDIRECT: {
|
||||
// we keep the current state until we are redirected to the new page
|
||||
return {
|
||||
...state,
|
||||
redirecting: true
|
||||
};
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
@@ -89,10 +97,16 @@ export const logoutPending = () => {
|
||||
|
||||
export const logoutSuccess = () => {
|
||||
return {
|
||||
type: LOGOUT_SUCCESS
|
||||
type: LOGOUT_SUCCESS,
|
||||
};
|
||||
};
|
||||
|
||||
export const redirectAfterLogout = () => {
|
||||
return {
|
||||
type: LOGOUT_REDIRECT
|
||||
}
|
||||
};
|
||||
|
||||
export const logoutFailure = (error: Error) => {
|
||||
return {
|
||||
type: LOGOUT_FAILURE,
|
||||
@@ -130,11 +144,9 @@ export const fetchMeFailure = (error: Error) => {
|
||||
// side effects
|
||||
|
||||
const callFetchMe = (link: string): Promise<Me> => {
|
||||
return apiClient
|
||||
.get(link)
|
||||
.then(response => {
|
||||
return response.json();
|
||||
});
|
||||
return apiClient.get(link).then(response => {
|
||||
return response.json();
|
||||
});
|
||||
};
|
||||
|
||||
export const login = (
|
||||
@@ -192,11 +204,28 @@ export const logout = (link: string) => {
|
||||
dispatch(logoutPending());
|
||||
return apiClient
|
||||
.delete(link)
|
||||
.then(() => {
|
||||
dispatch(logoutSuccess());
|
||||
.then(response => {
|
||||
return response.status === 200
|
||||
? response.json()
|
||||
: new Promise(function(resolve) {
|
||||
resolve();
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
dispatch(fetchIndexResources());
|
||||
.then(json => {
|
||||
let fetchIndex = true;
|
||||
if (json && json.logoutRedirect) {
|
||||
dispatch(redirectAfterLogout());
|
||||
window.location.assign(json.logoutRedirect);
|
||||
fetchIndex = false;
|
||||
} else {
|
||||
dispatch(logoutSuccess());
|
||||
}
|
||||
return fetchIndex;
|
||||
})
|
||||
.then((fetchIndex: boolean) => {
|
||||
if (fetchIndex) {
|
||||
dispatch(fetchIndexResources());
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
dispatch(logoutFailure(error));
|
||||
@@ -244,3 +273,8 @@ export const isLogoutPending = (state: Object) => {
|
||||
export const getLogoutFailure = (state: Object) => {
|
||||
return getFailure(state, LOGOUT);
|
||||
};
|
||||
|
||||
export const isRedirecting = (state: Object) => {
|
||||
return !!stateAuth(state).redirecting;
|
||||
};
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ import reducer, {
|
||||
FETCH_ME,
|
||||
LOGOUT,
|
||||
getLoginFailure,
|
||||
getLogoutFailure
|
||||
getLogoutFailure, isRedirecting, LOGOUT_REDIRECT, redirectAfterLogout,
|
||||
} from "./auth";
|
||||
|
||||
import configureMockStore from "redux-mock-store";
|
||||
@@ -70,6 +70,17 @@ describe("auth reducer", () => {
|
||||
expect(state.authenticated).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should keep state and set redirecting to true", () => {
|
||||
const initialState = {
|
||||
authenticated: true,
|
||||
me
|
||||
};
|
||||
const state = reducer(initialState, redirectAfterLogout());
|
||||
expect(state.me).toBe(initialState.me);
|
||||
expect(state.authenticated).toBe(initialState.authenticated);
|
||||
expect(state.redirecting).toBe(true);
|
||||
});
|
||||
|
||||
it("should set state authenticated and me after login", () => {
|
||||
const state = reducer(undefined, loginSuccess(me));
|
||||
expect(state.me).toBe(me);
|
||||
@@ -224,6 +235,41 @@ describe("auth actions", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should dispatch logout success and redirect", () => {
|
||||
fetchMock.deleteOnce("/api/v2/auth/access_token", {
|
||||
status: 200,
|
||||
body: { logoutRedirect: "http://example.com/cas/logout" }
|
||||
});
|
||||
|
||||
fetchMock.getOnce("/api/v2/me", {
|
||||
status: 401
|
||||
});
|
||||
|
||||
fetchMock.getOnce("/api/v2/", {
|
||||
_links: {
|
||||
login: {
|
||||
login: "/login"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
window.location.assign = jest.fn();
|
||||
|
||||
const expectedActions = [
|
||||
{ type: LOGOUT_PENDING },
|
||||
{ type: LOGOUT_REDIRECT }
|
||||
];
|
||||
|
||||
const store = mockStore({});
|
||||
|
||||
return store.dispatch(logout("/auth/access_token")).then(() => {
|
||||
expect(window.location.assign.mock.calls[0][0]).toBe(
|
||||
"http://example.com/cas/logout"
|
||||
);
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
it("should dispatch logout failure", () => {
|
||||
fetchMock.deleteOnce("/api/v2/auth/access_token", {
|
||||
status: 500
|
||||
@@ -307,4 +353,16 @@ describe("auth selectors", () => {
|
||||
it("should return unknown, if failure state is not set for LOGOUT", () => {
|
||||
expect(getLogoutFailure({})).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should return false, if redirecting is not set", () => {
|
||||
expect(isRedirecting({})).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false, if redirecting is false", () => {
|
||||
expect(isRedirecting({auth: { redirecting: false }})).toBe(false);
|
||||
});
|
||||
|
||||
it("should return true, if redirecting is true", () => {
|
||||
expect(isRedirecting({auth: { redirecting: true }})).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
49
scm-ui/src/repos/branches/components/BranchButtonGroup.js
Normal file
49
scm-ui/src/repos/branches/components/BranchButtonGroup.js
Normal file
@@ -0,0 +1,49 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import type { Repository, Branch } from "@scm-manager/ui-types";
|
||||
import { ButtonGroup, Button } from "@scm-manager/ui-components";
|
||||
import { translate } from "react-i18next";
|
||||
|
||||
type Props = {
|
||||
repository: Repository,
|
||||
branch: Branch,
|
||||
|
||||
// context props
|
||||
t: string => string
|
||||
};
|
||||
|
||||
class BranchButtonGroup extends React.Component<Props> {
|
||||
render() {
|
||||
const { repository, branch, t } = this.props;
|
||||
|
||||
const changesetLink = `/repo/${repository.namespace}/${
|
||||
repository.name
|
||||
}/branch/${encodeURIComponent(branch.name)}/changesets/`;
|
||||
const sourcesLink = `/repo/${repository.namespace}/${
|
||||
repository.name
|
||||
}/sources/${encodeURIComponent(branch.name)}/`;
|
||||
|
||||
return (
|
||||
<ButtonGroup>
|
||||
<Button link={changesetLink}>
|
||||
<span className="icon">
|
||||
<i className="fas fa-exchange-alt" />
|
||||
</span>
|
||||
<span className="is-hidden-mobile is-hidden-tablet-only">
|
||||
{t("branch.commits")}
|
||||
</span>
|
||||
</Button>
|
||||
<Button link={sourcesLink}>
|
||||
<span className="icon">
|
||||
<i className="fas fa-code" />
|
||||
</span>
|
||||
<span className="is-hidden-mobile is-hidden-tablet-only">
|
||||
{t("branch.sources")}
|
||||
</span>
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate("repos")(BranchButtonGroup);
|
||||
33
scm-ui/src/repos/branches/components/BranchDetail.js
Normal file
33
scm-ui/src/repos/branches/components/BranchDetail.js
Normal file
@@ -0,0 +1,33 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import type { Repository, Branch } from "@scm-manager/ui-types";
|
||||
import { translate } from "react-i18next";
|
||||
import BranchButtonGroup from "./BranchButtonGroup";
|
||||
import DefaultBranchTag from "./DefaultBranchTag";
|
||||
|
||||
type Props = {
|
||||
repository: Repository,
|
||||
branch: Branch,
|
||||
// context props
|
||||
t: string => string
|
||||
};
|
||||
|
||||
class BranchDetail extends React.Component<Props> {
|
||||
render() {
|
||||
const { repository, branch, t } = this.props;
|
||||
|
||||
return (
|
||||
<div className="media">
|
||||
<div className="media-content subtitle">
|
||||
<strong>{t("branch.name")}</strong> {branch.name}{" "}
|
||||
<DefaultBranchTag defaultBranch={branch.defaultBranch} />
|
||||
</div>
|
||||
<div className="media-right">
|
||||
<BranchButtonGroup repository={repository} branch={branch} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate("repos")(BranchDetail);
|
||||
125
scm-ui/src/repos/branches/components/BranchForm.js
Normal file
125
scm-ui/src/repos/branches/components/BranchForm.js
Normal file
@@ -0,0 +1,125 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import type { Repository, Branch, BranchRequest } from "@scm-manager/ui-types";
|
||||
import {
|
||||
Select,
|
||||
InputField,
|
||||
SubmitButton,
|
||||
validation as validator
|
||||
} from "@scm-manager/ui-components";
|
||||
import { orderBranches } from "../util/orderBranches";
|
||||
|
||||
type Props = {
|
||||
submitForm: BranchRequest => void,
|
||||
repository: Repository,
|
||||
branches: Branch[],
|
||||
loading?: boolean,
|
||||
transmittedName?: string,
|
||||
disabled?: boolean,
|
||||
t: string => string
|
||||
};
|
||||
|
||||
type State = {
|
||||
source?: string,
|
||||
name?: string,
|
||||
nameValidationError: boolean
|
||||
};
|
||||
|
||||
class BranchForm extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
nameValidationError: false,
|
||||
name: props.transmittedName
|
||||
};
|
||||
}
|
||||
|
||||
isFalsy(value) {
|
||||
return !value;
|
||||
}
|
||||
|
||||
isValid = () => {
|
||||
const { source, name } = this.state;
|
||||
return !(
|
||||
this.state.nameValidationError ||
|
||||
this.isFalsy(source) ||
|
||||
this.isFalsy(name)
|
||||
);
|
||||
};
|
||||
|
||||
submit = (event: Event) => {
|
||||
event.preventDefault();
|
||||
if (this.isValid()) {
|
||||
this.props.submitForm({
|
||||
name: this.state.name,
|
||||
parent: this.state.source
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t, branches, loading, transmittedName, disabled } = this.props;
|
||||
const { name } = this.state;
|
||||
orderBranches(branches);
|
||||
const options = branches.map(branch => ({
|
||||
label: branch.name,
|
||||
value: branch.name
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
<form onSubmit={this.submit}>
|
||||
<div className="columns">
|
||||
<div className="column">
|
||||
<Select
|
||||
name="source"
|
||||
label={t("branches.create.source")}
|
||||
options={options}
|
||||
onChange={this.handleSourceChange}
|
||||
loading={loading}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<InputField
|
||||
name="name"
|
||||
label={t("branches.create.name")}
|
||||
onChange={this.handleNameChange}
|
||||
value={name ? name : ""}
|
||||
validationError={this.state.nameValidationError}
|
||||
errorMessage={t("validation.branch.nameInvalid")}
|
||||
disabled={!!transmittedName || disabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="columns">
|
||||
<div className="column">
|
||||
<SubmitButton
|
||||
disabled={disabled || !this.isValid()}
|
||||
loading={loading}
|
||||
label={t("branches.create.submit")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
handleSourceChange = (source: string) => {
|
||||
this.setState({
|
||||
...this.state,
|
||||
source
|
||||
});
|
||||
};
|
||||
|
||||
handleNameChange = (name: string) => {
|
||||
this.setState({
|
||||
nameValidationError: !validator.isNameValid(name),
|
||||
...this.state,
|
||||
name
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export default translate("repos")(BranchForm);
|
||||
32
scm-ui/src/repos/branches/components/BranchRow.js
Normal file
32
scm-ui/src/repos/branches/components/BranchRow.js
Normal file
@@ -0,0 +1,32 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import type { Branch } from "@scm-manager/ui-types";
|
||||
import DefaultBranchTag from "./DefaultBranchTag";
|
||||
|
||||
type Props = {
|
||||
baseUrl: string,
|
||||
branch: Branch
|
||||
};
|
||||
|
||||
class BranchRow extends React.Component<Props> {
|
||||
renderLink(to: string, label: string, defaultBranch?: boolean) {
|
||||
return (
|
||||
<Link to={to}>
|
||||
{label} <DefaultBranchTag defaultBranch={defaultBranch} />
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { baseUrl, branch } = this.props;
|
||||
const to = `${baseUrl}/${encodeURIComponent(branch.name)}/info`;
|
||||
return (
|
||||
<tr>
|
||||
<td>{this.renderLink(to, branch.name, branch.defaultBranch)}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default BranchRow;
|
||||
40
scm-ui/src/repos/branches/components/BranchTable.js
Normal file
40
scm-ui/src/repos/branches/components/BranchTable.js
Normal file
@@ -0,0 +1,40 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import BranchRow from "./BranchRow";
|
||||
import type { Branch } from "@scm-manager/ui-types";
|
||||
|
||||
type Props = {
|
||||
baseUrl: string,
|
||||
t: string => string,
|
||||
branches: Branch[]
|
||||
};
|
||||
|
||||
class BranchTable extends React.Component<Props> {
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
return (
|
||||
<table className="card-table table is-hoverable is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t("branches.table.branches")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{this.renderRow()}</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
renderRow() {
|
||||
const { baseUrl, branches } = this.props;
|
||||
let rowContent = null;
|
||||
if (branches) {
|
||||
rowContent = branches.map((branch, index) => {
|
||||
return <BranchRow key={index} baseUrl={baseUrl} branch={branch} />;
|
||||
});
|
||||
}
|
||||
return rowContent;
|
||||
}
|
||||
}
|
||||
|
||||
export default translate("repos")(BranchTable);
|
||||
32
scm-ui/src/repos/branches/components/BranchView.js
Normal file
32
scm-ui/src/repos/branches/components/BranchView.js
Normal file
@@ -0,0 +1,32 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import BranchDetail from "./BranchDetail";
|
||||
import { ExtensionPoint } from "@scm-manager/ui-extensions";
|
||||
import type { Repository, Branch } from "@scm-manager/ui-types";
|
||||
|
||||
type Props = {
|
||||
repository: Repository,
|
||||
branch: Branch
|
||||
};
|
||||
|
||||
class BranchView extends React.Component<Props> {
|
||||
render() {
|
||||
const { repository, branch } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<BranchDetail repository={repository} branch={branch} />
|
||||
<hr />
|
||||
<div className="content">
|
||||
<ExtensionPoint
|
||||
name="repos.branch-details.information"
|
||||
renderAll={true}
|
||||
props={{ repository, branch }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default BranchView;
|
||||
35
scm-ui/src/repos/branches/components/DefaultBranchTag.js
Normal file
35
scm-ui/src/repos/branches/components/DefaultBranchTag.js
Normal file
@@ -0,0 +1,35 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import injectSheet from "react-jss";
|
||||
import classNames from "classnames";
|
||||
import { translate } from "react-i18next";
|
||||
|
||||
type Props = {
|
||||
defaultBranch?: boolean,
|
||||
classes: any,
|
||||
t: string => string
|
||||
};
|
||||
|
||||
const styles = {
|
||||
tag: {
|
||||
marginLeft: "0.75rem",
|
||||
verticalAlign: "inherit"
|
||||
}
|
||||
};
|
||||
|
||||
class DefaultBranchTag extends React.Component<Props> {
|
||||
render() {
|
||||
const { defaultBranch, classes, t } = this.props;
|
||||
|
||||
if (defaultBranch) {
|
||||
return (
|
||||
<span className={classNames("tag is-dark", classes.tag)}>
|
||||
{t("branch.defaultTag")}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default injectSheet(styles)(translate("repos")(DefaultBranchTag));
|
||||
120
scm-ui/src/repos/branches/containers/BranchRoot.js
Normal file
120
scm-ui/src/repos/branches/containers/BranchRoot.js
Normal file
@@ -0,0 +1,120 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import BranchView from "../components/BranchView";
|
||||
import { connect } from "react-redux";
|
||||
import { Redirect, Route, Switch, withRouter } from "react-router-dom";
|
||||
import type { Repository, Branch } from "@scm-manager/ui-types";
|
||||
import {
|
||||
fetchBranch,
|
||||
getBranch,
|
||||
getFetchBranchFailure,
|
||||
isFetchBranchPending
|
||||
} from "../modules/branches";
|
||||
import { ErrorNotification, Loading } from "@scm-manager/ui-components";
|
||||
import type { History } from "history";
|
||||
import { NotFoundError } from "@scm-manager/ui-components";
|
||||
import queryString from "query-string";
|
||||
|
||||
type Props = {
|
||||
repository: Repository,
|
||||
branchName: string,
|
||||
branch: Branch,
|
||||
loading: boolean,
|
||||
error?: Error,
|
||||
|
||||
// context props
|
||||
history: History,
|
||||
match: any,
|
||||
location: any,
|
||||
|
||||
// dispatch functions
|
||||
fetchBranch: (repository: Repository, branchName: string) => void
|
||||
};
|
||||
|
||||
class BranchRoot extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
const { fetchBranch, repository, branchName } = this.props;
|
||||
|
||||
fetchBranch(repository, branchName);
|
||||
}
|
||||
|
||||
stripEndingSlash = (url: string) => {
|
||||
if (url.endsWith("/")) {
|
||||
return url.substring(0, url.length - 1);
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
matchedUrl = () => {
|
||||
return this.stripEndingSlash(this.props.match.url);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { repository, branch, loading, error, match, location } = this.props;
|
||||
|
||||
const url = this.matchedUrl();
|
||||
|
||||
if (error) {
|
||||
if (
|
||||
error instanceof NotFoundError &&
|
||||
queryString.parse(location.search).create === "true"
|
||||
) {
|
||||
return (
|
||||
<Redirect
|
||||
to={`/repo/${repository.namespace}/${
|
||||
repository.name
|
||||
}/branches/create?name=${match.params.branch}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <ErrorNotification error={error} />;
|
||||
}
|
||||
|
||||
if (loading || !branch) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Redirect exact from={url} to={`${url}/info`} />
|
||||
<Route
|
||||
path={`${url}/info`}
|
||||
component={() => (
|
||||
<BranchView repository={repository} branch={branch} />
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const { repository } = ownProps;
|
||||
const branchName = decodeURIComponent(ownProps.match.params.branch);
|
||||
const branch = getBranch(state, repository, branchName);
|
||||
const loading = isFetchBranchPending(state, repository, branchName);
|
||||
const error = getFetchBranchFailure(state, repository, branchName);
|
||||
return {
|
||||
repository,
|
||||
branchName,
|
||||
branch,
|
||||
loading,
|
||||
error
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
fetchBranch: (repository: Repository, branchName: string) => {
|
||||
dispatch(fetchBranch(repository, branchName));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default withRouter(
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(BranchRoot)
|
||||
);
|
||||
122
scm-ui/src/repos/branches/containers/BranchesOverview.js
Normal file
122
scm-ui/src/repos/branches/containers/BranchesOverview.js
Normal file
@@ -0,0 +1,122 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import {
|
||||
fetchBranches,
|
||||
getBranches,
|
||||
getFetchBranchesFailure,
|
||||
isFetchBranchesPending,
|
||||
isPermittedToCreateBranches
|
||||
} from "../modules/branches";
|
||||
import { orderBranches } from "../util/orderBranches";
|
||||
import { connect } from "react-redux";
|
||||
import type { Branch, Repository } from "@scm-manager/ui-types";
|
||||
import { compose } from "redux";
|
||||
import { translate } from "react-i18next";
|
||||
import { withRouter } from "react-router-dom";
|
||||
import {
|
||||
CreateButton,
|
||||
ErrorNotification,
|
||||
Loading,
|
||||
Notification,
|
||||
Subtitle
|
||||
} from "@scm-manager/ui-components";
|
||||
import BranchTable from "../components/BranchTable";
|
||||
|
||||
type Props = {
|
||||
repository: Repository,
|
||||
baseUrl: string,
|
||||
loading: boolean,
|
||||
error: Error,
|
||||
branches: Branch[],
|
||||
|
||||
// dispatch props
|
||||
showCreateButton: boolean,
|
||||
fetchBranches: Repository => void,
|
||||
|
||||
// Context props
|
||||
history: any,
|
||||
match: any,
|
||||
t: string => string
|
||||
};
|
||||
|
||||
class BranchesOverview extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
const { fetchBranches, repository } = this.props;
|
||||
fetchBranches(repository);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { loading, error, branches, t } = this.props;
|
||||
|
||||
if (error) {
|
||||
return <ErrorNotification error={error} />;
|
||||
}
|
||||
|
||||
if (!branches || loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Subtitle subtitle={t("branches.overview.title")} />
|
||||
{this.renderBranchesTable()}
|
||||
{this.renderCreateButton()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
renderBranchesTable() {
|
||||
const { baseUrl, branches, t } = this.props;
|
||||
if (branches && branches.length > 0) {
|
||||
orderBranches(branches);
|
||||
return <BranchTable baseUrl={baseUrl} branches={branches} />;
|
||||
}
|
||||
return <Notification type="info">{t("branches.overview.noBranches")}</Notification>;
|
||||
}
|
||||
|
||||
renderCreateButton() {
|
||||
const { showCreateButton, t } = this.props;
|
||||
if (showCreateButton) {
|
||||
return (
|
||||
<CreateButton
|
||||
label={t("branches.overview.createButton")}
|
||||
link="./create"
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const { repository } = ownProps;
|
||||
const loading = isFetchBranchesPending(state, repository);
|
||||
const error = getFetchBranchesFailure(state, repository);
|
||||
const branches = getBranches(state, repository);
|
||||
const showCreateButton = isPermittedToCreateBranches(state, repository);
|
||||
|
||||
return {
|
||||
repository,
|
||||
loading,
|
||||
error,
|
||||
branches,
|
||||
showCreateButton
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
fetchBranches: (repository: Repository) => {
|
||||
dispatch(fetchBranches(repository));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default compose(
|
||||
translate("repos"),
|
||||
withRouter,
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)
|
||||
)(BranchesOverview);
|
||||
150
scm-ui/src/repos/branches/containers/CreateBranch.js
Normal file
150
scm-ui/src/repos/branches/containers/CreateBranch.js
Normal file
@@ -0,0 +1,150 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import {
|
||||
ErrorNotification,
|
||||
Loading,
|
||||
Subtitle
|
||||
} from "@scm-manager/ui-components";
|
||||
import { translate } from "react-i18next";
|
||||
import BranchForm from "../components/BranchForm";
|
||||
import type { Repository, Branch, BranchRequest } from "@scm-manager/ui-types";
|
||||
import {
|
||||
fetchBranches,
|
||||
getBranches,
|
||||
getBranchCreateLink,
|
||||
createBranch,
|
||||
createBranchReset,
|
||||
isCreateBranchPending,
|
||||
getCreateBranchFailure,
|
||||
isFetchBranchesPending,
|
||||
getFetchBranchesFailure
|
||||
} from "../modules/branches";
|
||||
import type { History } from "history";
|
||||
import { connect } from "react-redux";
|
||||
import { withRouter } from "react-router-dom";
|
||||
import queryString from "query-string";
|
||||
|
||||
type Props = {
|
||||
loading?: boolean,
|
||||
error?: Error,
|
||||
repository: Repository,
|
||||
branches: Branch[],
|
||||
createBranchesLink: string,
|
||||
isPermittedToCreateBranches: boolean,
|
||||
|
||||
// dispatcher functions
|
||||
fetchBranches: Repository => void,
|
||||
createBranch: (
|
||||
createLink: string,
|
||||
repository: Repository,
|
||||
branch: BranchRequest,
|
||||
callback?: (Branch) => void
|
||||
) => void,
|
||||
resetForm: Repository => void,
|
||||
|
||||
// context objects
|
||||
t: string => string,
|
||||
history: History,
|
||||
location: any
|
||||
};
|
||||
|
||||
class CreateBranch extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
const { fetchBranches, repository } = this.props;
|
||||
fetchBranches(repository);
|
||||
this.props.resetForm(repository);
|
||||
}
|
||||
|
||||
branchCreated = (branch: Branch) => {
|
||||
const { history, repository } = this.props;
|
||||
history.push(
|
||||
`/repo/${repository.namespace}/${
|
||||
repository.name
|
||||
}/branch/${encodeURIComponent(branch.name)}/info`
|
||||
);
|
||||
};
|
||||
|
||||
createBranch = (branch: BranchRequest) => {
|
||||
this.props.createBranch(
|
||||
this.props.createBranchesLink,
|
||||
this.props.repository,
|
||||
branch,
|
||||
newBranch => this.branchCreated(newBranch)
|
||||
);
|
||||
};
|
||||
|
||||
transmittedName = (url: string) => {
|
||||
const params = queryString.parse(url);
|
||||
return params.name;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t, loading, error, repository, branches, createBranchesLink, location } = this.props;
|
||||
|
||||
if (error) {
|
||||
return <ErrorNotification error={error} />;
|
||||
}
|
||||
|
||||
if (loading || !branches) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Subtitle subtitle={t("branches.create.title")} />
|
||||
<BranchForm
|
||||
submitForm={branchRequest => this.createBranch(branchRequest)}
|
||||
loading={loading}
|
||||
repository={repository}
|
||||
branches={branches}
|
||||
transmittedName={this.transmittedName(location.search)}
|
||||
disabled={!createBranchesLink}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
fetchBranches: (repository: Repository) => {
|
||||
dispatch(fetchBranches(repository));
|
||||
},
|
||||
createBranch: (
|
||||
createLink: string,
|
||||
repository: Repository,
|
||||
branchRequest: BranchRequest,
|
||||
callback?: (newBranch: Branch) => void
|
||||
) => {
|
||||
dispatch(createBranch(createLink, repository, branchRequest, callback));
|
||||
},
|
||||
resetForm: (repository: Repository) => {
|
||||
dispatch(createBranchReset(repository));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const { repository } = ownProps;
|
||||
const loading =
|
||||
isFetchBranchesPending(state, repository) ||
|
||||
isCreateBranchPending(state, repository);
|
||||
const error =
|
||||
getFetchBranchesFailure(state, repository) || getCreateBranchFailure(state);
|
||||
const branches = getBranches(state, repository);
|
||||
const createBranchesLink = getBranchCreateLink(state, repository);
|
||||
return {
|
||||
repository,
|
||||
loading,
|
||||
error,
|
||||
branches,
|
||||
createBranchesLink
|
||||
};
|
||||
};
|
||||
|
||||
export default withRouter(
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(translate("repos")(CreateBranch))
|
||||
);
|
||||
361
scm-ui/src/repos/branches/modules/branches.js
Normal file
361
scm-ui/src/repos/branches/modules/branches.js
Normal file
@@ -0,0 +1,361 @@
|
||||
// @flow
|
||||
import {
|
||||
FAILURE_SUFFIX,
|
||||
PENDING_SUFFIX,
|
||||
SUCCESS_SUFFIX,
|
||||
RESET_SUFFIX
|
||||
} from "../../../modules/types";
|
||||
import { apiClient } from "@scm-manager/ui-components";
|
||||
import type {
|
||||
Action,
|
||||
Branch,
|
||||
BranchRequest,
|
||||
Repository
|
||||
} from "@scm-manager/ui-types";
|
||||
import { isPending } from "../../../modules/pending";
|
||||
import { getFailure } from "../../../modules/failure";
|
||||
|
||||
import memoizeOne from 'memoize-one';
|
||||
|
||||
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}`;
|
||||
|
||||
export const FETCH_BRANCH = "scm/repos/FETCH_BRANCH";
|
||||
export const FETCH_BRANCH_PENDING = `${FETCH_BRANCH}_${PENDING_SUFFIX}`;
|
||||
export const FETCH_BRANCH_SUCCESS = `${FETCH_BRANCH}_${SUCCESS_SUFFIX}`;
|
||||
export const FETCH_BRANCH_FAILURE = `${FETCH_BRANCH}_${FAILURE_SUFFIX}`;
|
||||
|
||||
export const CREATE_BRANCH = "scm/repos/CREATE_BRANCH";
|
||||
export const CREATE_BRANCH_PENDING = `${CREATE_BRANCH}_${PENDING_SUFFIX}`;
|
||||
export const CREATE_BRANCH_SUCCESS = `${CREATE_BRANCH}_${SUCCESS_SUFFIX}`;
|
||||
export const CREATE_BRANCH_FAILURE = `${CREATE_BRANCH}_${FAILURE_SUFFIX}`;
|
||||
export const CREATE_BRANCH_RESET = `${CREATE_BRANCH}_${RESET_SUFFIX}`;
|
||||
|
||||
const CONTENT_TYPE_BRANCH_REQUEST =
|
||||
"application/vnd.scmm-branchRequest+json;v=2";
|
||||
|
||||
// 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));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchBranch(repository: Repository, name: string) {
|
||||
let link = repository._links.branches.href;
|
||||
if (!link.endsWith("/")) {
|
||||
link += "/";
|
||||
}
|
||||
link += encodeURIComponent(name);
|
||||
return function(dispatch: any) {
|
||||
dispatch(fetchBranchPending(repository, name));
|
||||
return apiClient
|
||||
.get(link)
|
||||
.then(response => {
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
dispatch(fetchBranchSuccess(repository, data));
|
||||
})
|
||||
.catch(error => {
|
||||
dispatch(fetchBranchFailure(repository, name, error));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// create branch
|
||||
|
||||
export function createBranch(
|
||||
link: string,
|
||||
repository: Repository,
|
||||
branchRequest: BranchRequest,
|
||||
callback?: (branch: Branch) => void
|
||||
) {
|
||||
return function(dispatch: any) {
|
||||
dispatch(createBranchPending(repository));
|
||||
return apiClient
|
||||
.post(link, branchRequest, CONTENT_TYPE_BRANCH_REQUEST)
|
||||
.then(response => response.headers.get("Location"))
|
||||
.then(location => apiClient.get(location))
|
||||
.then(response => response.json())
|
||||
.then(branch => {
|
||||
dispatch(createBranchSuccess(repository));
|
||||
if (callback) {
|
||||
callback(branch);
|
||||
}
|
||||
})
|
||||
.catch(error => dispatch(createBranchFailure(repository, error)));
|
||||
};
|
||||
}
|
||||
|
||||
// Selectors
|
||||
|
||||
function collectBranches(repoState) {
|
||||
return repoState.list._embedded.branches.map(
|
||||
name => repoState.byName[name]
|
||||
);
|
||||
}
|
||||
|
||||
const memoizedBranchCollector = memoizeOne(collectBranches);
|
||||
|
||||
export function getBranches(state: Object, repository: Repository) {
|
||||
const repoState = getRepoState(state, repository);
|
||||
if (repoState && repoState.list) {
|
||||
return memoizedBranchCollector(repoState);
|
||||
}
|
||||
}
|
||||
|
||||
export function getBranchCreateLink(state: Object, repository: Repository) {
|
||||
const repoState = getRepoState(state, repository);
|
||||
if (repoState && repoState.list && repoState.list._links && repoState.list._links.create) {
|
||||
return repoState.list._links.create.href;
|
||||
}
|
||||
}
|
||||
|
||||
function getRepoState(state: Object, repository: Repository) {
|
||||
const key = createKey(repository);
|
||||
const repoState = state.branches[key];
|
||||
if (repoState && repoState.byName) {
|
||||
return repoState;
|
||||
}
|
||||
}
|
||||
|
||||
export const isPermittedToCreateBranches = (
|
||||
state: Object,
|
||||
repository: Repository
|
||||
): boolean => {
|
||||
const repoState = getRepoState(state, repository);
|
||||
return !!(
|
||||
repoState &&
|
||||
repoState.list &&
|
||||
repoState.list._links &&
|
||||
repoState.list._links.create
|
||||
);
|
||||
};
|
||||
|
||||
export function getBranch(
|
||||
state: Object,
|
||||
repository: Repository,
|
||||
name: string
|
||||
): ?Branch {
|
||||
const repoState = getRepoState(state, repository);
|
||||
if (repoState) {
|
||||
return repoState.byName[name];
|
||||
}
|
||||
}
|
||||
|
||||
// Action creators
|
||||
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));
|
||||
}
|
||||
|
||||
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)
|
||||
};
|
||||
}
|
||||
|
||||
export function isCreateBranchPending(state: Object, repository: Repository) {
|
||||
return isPending(state, CREATE_BRANCH, createKey(repository));
|
||||
}
|
||||
|
||||
export function getCreateBranchFailure(state: Object) {
|
||||
return getFailure(state, CREATE_BRANCH);
|
||||
}
|
||||
|
||||
export function createBranchPending(repository: Repository): Action {
|
||||
return {
|
||||
type: CREATE_BRANCH_PENDING,
|
||||
payload: { repository },
|
||||
itemId: createKey(repository)
|
||||
};
|
||||
}
|
||||
|
||||
export function createBranchSuccess(repository: Repository): Action {
|
||||
return {
|
||||
type: CREATE_BRANCH_SUCCESS,
|
||||
payload: { repository },
|
||||
itemId: createKey(repository)
|
||||
};
|
||||
}
|
||||
|
||||
export function createBranchFailure(
|
||||
repository: Repository,
|
||||
error: Error
|
||||
): Action {
|
||||
return {
|
||||
type: CREATE_BRANCH_FAILURE,
|
||||
payload: { repository, error },
|
||||
itemId: createKey(repository)
|
||||
};
|
||||
}
|
||||
|
||||
export function createBranchReset(repository: Repository): Action {
|
||||
return {
|
||||
type: CREATE_BRANCH_RESET,
|
||||
payload: { repository },
|
||||
itemId: createKey(repository)
|
||||
};
|
||||
}
|
||||
|
||||
export function isFetchBranchPending(
|
||||
state: Object,
|
||||
repository: Repository,
|
||||
name: string
|
||||
) {
|
||||
return isPending(state, FETCH_BRANCH, createKey(repository) + "/" + name);
|
||||
}
|
||||
|
||||
export function getFetchBranchFailure(
|
||||
state: Object,
|
||||
repository: Repository,
|
||||
name: string
|
||||
) {
|
||||
return getFailure(state, FETCH_BRANCH, createKey(repository) + "/" + name);
|
||||
}
|
||||
|
||||
export function fetchBranchPending(
|
||||
repository: Repository,
|
||||
name: string
|
||||
): Action {
|
||||
return {
|
||||
type: FETCH_BRANCH_PENDING,
|
||||
payload: { repository, name },
|
||||
itemId: createKey(repository) + "/" + name
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchBranchSuccess(
|
||||
repository: Repository,
|
||||
branch: Branch
|
||||
): Action {
|
||||
return {
|
||||
type: FETCH_BRANCH_SUCCESS,
|
||||
payload: { repository, branch },
|
||||
itemId: createKey(repository) + "/" + branch.name
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchBranchFailure(
|
||||
repository: Repository,
|
||||
name: string,
|
||||
error: Error
|
||||
): Action {
|
||||
return {
|
||||
type: FETCH_BRANCH_FAILURE,
|
||||
payload: { error, repository, name },
|
||||
itemId: createKey(repository) + "/" + name
|
||||
};
|
||||
}
|
||||
|
||||
// Reducers
|
||||
|
||||
const reduceByBranchesSuccess = (state, payload) => {
|
||||
const repository = payload.repository;
|
||||
const response = payload.data;
|
||||
|
||||
const key = createKey(repository);
|
||||
const repoState = state[key] || {};
|
||||
const byName = repoState.byName || {};
|
||||
repoState.byName = byName;
|
||||
|
||||
const branches = response._embedded.branches;
|
||||
const names = branches.map(b => b.name);
|
||||
response._embedded.branches = names;
|
||||
for (let branch of branches) {
|
||||
byName[branch.name] = branch;
|
||||
}
|
||||
return {
|
||||
[key]: {
|
||||
list: response,
|
||||
byName
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const reduceByBranchSuccess = (state, payload) => {
|
||||
const repository = payload.repository;
|
||||
const branch = payload.branch;
|
||||
|
||||
const key = createKey(repository);
|
||||
|
||||
const repoState = state[key] || {};
|
||||
|
||||
const byName = repoState.byName || {};
|
||||
byName[branch.name] = branch;
|
||||
|
||||
repoState.byName = byName;
|
||||
|
||||
return {
|
||||
...state,
|
||||
[key]: repoState
|
||||
};
|
||||
};
|
||||
|
||||
export default function reducer(
|
||||
state: {} = {},
|
||||
action: Action = { type: "UNKNOWN" }
|
||||
): Object {
|
||||
if (!action.payload) {
|
||||
return state;
|
||||
}
|
||||
const payload = action.payload;
|
||||
switch (action.type) {
|
||||
case FETCH_BRANCHES_SUCCESS:
|
||||
return reduceByBranchesSuccess(state, payload);
|
||||
case FETCH_BRANCH_SUCCESS:
|
||||
return reduceByBranchSuccess(state, payload);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
function createKey(repository: Repository): string {
|
||||
const { namespace, name } = repository;
|
||||
return `${namespace}/${name}`;
|
||||
}
|
||||
471
scm-ui/src/repos/branches/modules/branches.test.js
Normal file
471
scm-ui/src/repos/branches/modules/branches.test.js
Normal file
@@ -0,0 +1,471 @@
|
||||
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,
|
||||
FETCH_BRANCH_PENDING,
|
||||
FETCH_BRANCH_SUCCESS,
|
||||
FETCH_BRANCH_FAILURE,
|
||||
CREATE_BRANCH,
|
||||
CREATE_BRANCH_FAILURE,
|
||||
CREATE_BRANCH_PENDING,
|
||||
CREATE_BRANCH_SUCCESS,
|
||||
fetchBranches,
|
||||
fetchBranch,
|
||||
fetchBranchSuccess,
|
||||
getBranch,
|
||||
getBranches,
|
||||
getFetchBranchesFailure,
|
||||
isFetchBranchesPending,
|
||||
createBranch,
|
||||
isCreateBranchPending,
|
||||
getCreateBranchFailure,
|
||||
isPermittedToCreateBranches
|
||||
} 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" };
|
||||
const branchRequest = { name: "newBranch", source: "master" };
|
||||
const newBranch = { name: "newBranch", revision: "rev3" };
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
it("should successfully fetch single branch", () => {
|
||||
fetchMock.getOnce(URL + "/branch1", branch1);
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchBranch(repository, "branch1")).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(FETCH_BRANCH_PENDING);
|
||||
expect(actions[1].type).toEqual(FETCH_BRANCH_SUCCESS);
|
||||
expect(actions[1].payload).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail fetching single branch on HTTP 500", () => {
|
||||
fetchMock.getOnce(URL + "/branch2", {
|
||||
status: 500
|
||||
});
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchBranch(repository, "branch2")).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(FETCH_BRANCH_PENDING);
|
||||
expect(actions[1].type).toEqual(FETCH_BRANCH_FAILURE);
|
||||
expect(actions[1].payload).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("should create a branch successfully", () => {
|
||||
//branchrequest answer
|
||||
fetchMock.postOnce(URL, {
|
||||
status: 201,
|
||||
headers: {
|
||||
location: URL + "/newBranch"
|
||||
}
|
||||
});
|
||||
|
||||
//branch answer
|
||||
fetchMock.getOnce(URL + "/newBranch", newBranch);
|
||||
|
||||
const store = mockStore({});
|
||||
return store
|
||||
.dispatch(createBranch(URL, repository, branchRequest))
|
||||
.then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(CREATE_BRANCH_PENDING);
|
||||
expect(actions[1].type).toEqual(CREATE_BRANCH_SUCCESS);
|
||||
});
|
||||
});
|
||||
|
||||
it("should call the callback with the branch from the location header", () => {
|
||||
//branchrequest answer
|
||||
fetchMock.postOnce(URL, {
|
||||
status: 201,
|
||||
headers: {
|
||||
location: URL + "/newBranch"
|
||||
}
|
||||
});
|
||||
|
||||
//branch answer
|
||||
fetchMock.getOnce(URL + "/newBranch", newBranch);
|
||||
|
||||
const store = mockStore({});
|
||||
|
||||
let receivedBranch = null;
|
||||
|
||||
const callback = branch => {
|
||||
receivedBranch = branch;
|
||||
};
|
||||
|
||||
return store
|
||||
.dispatch(createBranch(URL, repository, branchRequest, callback))
|
||||
.then(() => {
|
||||
expect(receivedBranch).toEqual(newBranch);
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail creating a branch on HTTP 500", () => {
|
||||
fetchMock.postOnce(URL, {
|
||||
status: 500
|
||||
});
|
||||
|
||||
const store = mockStore({});
|
||||
return store
|
||||
.dispatch(createBranch(URL, repository, branchRequest))
|
||||
.then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(CREATE_BRANCH_PENDING);
|
||||
expect(actions[1].type).toEqual(CREATE_BRANCH_FAILURE);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("branches reducer", () => {
|
||||
const branches = {
|
||||
_embedded: {
|
||||
branches: [branch1, branch2]
|
||||
},
|
||||
_links: {
|
||||
self: {
|
||||
href: "/self"
|
||||
},
|
||||
create: {
|
||||
href: "/create"
|
||||
}
|
||||
}
|
||||
};
|
||||
const action = {
|
||||
type: FETCH_BRANCHES_SUCCESS,
|
||||
payload: {
|
||||
repository,
|
||||
data: branches
|
||||
}
|
||||
};
|
||||
|
||||
it("should store the branches", () => {
|
||||
const newState = reducer({}, action);
|
||||
const repoState = newState["foo/bar"];
|
||||
|
||||
expect(repoState.list._links.create.href).toEqual("/create");
|
||||
expect(repoState.list._embedded.branches).toEqual(["branch1", "branch2"]);
|
||||
|
||||
expect(repoState.byName.branch1).toEqual(branch1);
|
||||
expect(repoState.byName.branch2).toEqual(branch2);
|
||||
});
|
||||
|
||||
it("should store a single branch", () => {
|
||||
const newState = reducer({}, fetchBranchSuccess(repository, branch1));
|
||||
const repoState = newState["foo/bar"];
|
||||
|
||||
expect(repoState.list).toBeUndefined();
|
||||
expect(repoState.byName.branch1).toEqual(branch1);
|
||||
});
|
||||
|
||||
it("should add a single branch", () => {
|
||||
const state = {
|
||||
"foo/bar": {
|
||||
list: {
|
||||
_links: {},
|
||||
_embedded: {
|
||||
branches: ["branch1"]
|
||||
}
|
||||
},
|
||||
byName: {
|
||||
branch1: branch1
|
||||
}
|
||||
}
|
||||
};
|
||||
const newState = reducer(state, fetchBranchSuccess(repository, branch2));
|
||||
const repoState = newState["foo/bar"];
|
||||
const byName = repoState.byName;
|
||||
|
||||
expect(repoState.list._embedded.branches).toEqual(["branch1"]);
|
||||
expect(byName.branch1).toEqual(branch1);
|
||||
expect(byName.branch2).toEqual(branch2);
|
||||
});
|
||||
|
||||
it("should not overwrite non related repositories", () => {
|
||||
const state = {
|
||||
"scm/core": {
|
||||
byName: {
|
||||
branch1: branch1
|
||||
}
|
||||
}
|
||||
};
|
||||
const newState = reducer(state, fetchBranchSuccess(repository, branch1));
|
||||
const byName = newState["scm/core"].byName;
|
||||
|
||||
expect(byName.branch1).toEqual(branch1);
|
||||
});
|
||||
|
||||
it("should overwrite existing branch", () => {
|
||||
const state = {
|
||||
"foo/bar": {
|
||||
byName: {
|
||||
branch1: {
|
||||
name: "branch1",
|
||||
revision: "xyz"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const newState = reducer(state, fetchBranchSuccess(repository, branch1));
|
||||
const byName = newState["foo/bar"].byName;
|
||||
|
||||
expect(byName.branch1.revision).toEqual("revision1");
|
||||
});
|
||||
|
||||
it("should not overwrite existing branches", () => {
|
||||
const state = {
|
||||
"foo/bar": {
|
||||
byName: {
|
||||
branch1,
|
||||
branch2,
|
||||
branch3
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const newState = reducer(state, action);
|
||||
expect(newState["foo/bar"].byName.branch1).toEqual(branch1);
|
||||
expect(newState["foo/bar"].byName.branch2).toEqual(branch2);
|
||||
expect(newState["foo/bar"].byName.branch3).toEqual(branch3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("branch selectors", () => {
|
||||
const error = new Error("Something went wrong");
|
||||
|
||||
const state = {
|
||||
branches: {
|
||||
"foo/bar": {
|
||||
list: {
|
||||
_links: {},
|
||||
_embedded: {
|
||||
branches: ["branch1", "branch2"]
|
||||
}
|
||||
},
|
||||
byName: {
|
||||
branch1: branch1,
|
||||
branch2: 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 not return cached reference, if branches have changed", () => {
|
||||
const one = getBranches(state, repository);
|
||||
const newState = {
|
||||
branches: {
|
||||
"foo/bar": {
|
||||
list: {
|
||||
_links: {},
|
||||
_embedded: {
|
||||
branches: ["branch2", "branch3"]
|
||||
}
|
||||
},
|
||||
byName: {
|
||||
branch2,
|
||||
branch3
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const two = getBranches(newState, repository);
|
||||
expect(one).not.toBe(two);
|
||||
expect(two).not.toContain(branch1);
|
||||
expect(two).toContain(branch2);
|
||||
expect(two).toContain(branch3);
|
||||
});
|
||||
|
||||
it("should return undefined, if no branches for the repository available", () => {
|
||||
const branches = getBranches({ branches: {} }, repository);
|
||||
expect(branches).toBeUndefined();
|
||||
});
|
||||
|
||||
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 true if the branches list contains the create link", () => {
|
||||
const stateWithLink = {
|
||||
branches: {
|
||||
"foo/bar": {
|
||||
...state.branches["foo/bar"],
|
||||
list: {
|
||||
...state.branches["foo/bar"].list,
|
||||
_links: {
|
||||
create: {
|
||||
href: "http://create-it"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const permitted = isPermittedToCreateBranches(stateWithLink, repository);
|
||||
expect(permitted).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false if the create link is missing", () => {
|
||||
const permitted = isPermittedToCreateBranches(state, repository);
|
||||
expect(permitted).toBe(false);
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
it("should return true if create branch is pending", () => {
|
||||
const state = {
|
||||
pending: {
|
||||
[CREATE_BRANCH + "/foo/bar"]: true
|
||||
}
|
||||
};
|
||||
expect(isCreateBranchPending(state, repository)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false if create branch is not pending", () => {
|
||||
const state = {
|
||||
pending: {
|
||||
[CREATE_BRANCH + "/foo/bar"]: false
|
||||
}
|
||||
};
|
||||
expect(isCreateBranchPending(state, repository)).toBe(false);
|
||||
});
|
||||
|
||||
it("should return error when create branch did fail", () => {
|
||||
const state = {
|
||||
failure: {
|
||||
[CREATE_BRANCH]: error
|
||||
}
|
||||
};
|
||||
expect(getCreateBranchFailure(state)).toEqual(error);
|
||||
});
|
||||
|
||||
it("should return undefined when create branch did not fail", () => {
|
||||
expect(getCreateBranchFailure({})).toBe(undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
32
scm-ui/src/repos/branches/util/orderBranches.js
Normal file
32
scm-ui/src/repos/branches/util/orderBranches.js
Normal file
@@ -0,0 +1,32 @@
|
||||
// @flow
|
||||
|
||||
// master, default should always be the first one,
|
||||
// followed by develop the rest should be ordered by its name
|
||||
import type {Branch} from "@scm-manager/ui-types";
|
||||
|
||||
export function orderBranches(branches: Branch[]) {
|
||||
branches.sort((a, b) => {
|
||||
if (a.defaultBranch && !b.defaultBranch) {
|
||||
return -20;
|
||||
} else if (!a.defaultBranch && b.defaultBranch) {
|
||||
return 20;
|
||||
} else if (a.name === "master" && b.name !== "master") {
|
||||
return -10;
|
||||
} else if (a.name !== "master" && b.name === "master") {
|
||||
return 10;
|
||||
} else if (a.name === "default" && b.name !== "default") {
|
||||
return -10;
|
||||
} else if (a.name !== "default" && b.name === "default") {
|
||||
return 10;
|
||||
} else if (a.name === "develop" && b.name !== "develop") {
|
||||
return -5;
|
||||
} else if (a.name !== "develop" && b.name === "develop") {
|
||||
return 5;
|
||||
} else if (a.name < b.name) {
|
||||
return -1;
|
||||
} else if (a.name > b.name) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
51
scm-ui/src/repos/branches/util/orderBranches.test.js
Normal file
51
scm-ui/src/repos/branches/util/orderBranches.test.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import { orderBranches } from "./orderBranches";
|
||||
|
||||
const branch1 = { name: "branch1", revision: "revision1" };
|
||||
const branch2 = { name: "branch2", revision: "revision2" };
|
||||
const branch3 = { name: "branch3", revision: "revision3", defaultBranch: true };
|
||||
const defaultBranch = {
|
||||
name: "default",
|
||||
revision: "revision4",
|
||||
defaultBranch: false
|
||||
};
|
||||
const developBranch = {
|
||||
name: "develop",
|
||||
revision: "revision5",
|
||||
defaultBranch: false
|
||||
};
|
||||
const masterBranch = {
|
||||
name: "master",
|
||||
revision: "revision6",
|
||||
defaultBranch: false
|
||||
};
|
||||
|
||||
describe("order branches", () => {
|
||||
it("should return branches", () => {
|
||||
let branches = [branch1, branch2];
|
||||
orderBranches(branches);
|
||||
expect(branches).toEqual([branch1, branch2]);
|
||||
});
|
||||
|
||||
it("should return defaultBranch first", () => {
|
||||
let branches = [branch1, branch2, branch3];
|
||||
orderBranches(branches);
|
||||
expect(branches).toEqual([branch3, branch1, branch2]);
|
||||
});
|
||||
|
||||
it("should order special branches as follows: master > default > develop", () => {
|
||||
let branches = [defaultBranch, developBranch, masterBranch];
|
||||
orderBranches(branches);
|
||||
expect(branches).toEqual([masterBranch, defaultBranch, developBranch]);
|
||||
});
|
||||
|
||||
it("should order special branches but starting with defaultBranch", () => {
|
||||
let branches = [masterBranch, developBranch, defaultBranch, branch3];
|
||||
orderBranches(branches);
|
||||
expect(branches).toEqual([
|
||||
branch3,
|
||||
masterBranch,
|
||||
defaultBranch,
|
||||
developBranch
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -35,11 +35,23 @@ class RepositoryEntry extends React.Component<Props> {
|
||||
return `/repo/${repository.namespace}/${repository.name}`;
|
||||
};
|
||||
|
||||
renderBranchesLink = (repository: Repository, repositoryLink: string) => {
|
||||
if (repository._links["branches"]) {
|
||||
return (
|
||||
<RepositoryEntryLink
|
||||
iconClass="fas fa-code-branch fa-lg"
|
||||
to={repositoryLink + "/branches"}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
renderChangesetsLink = (repository: Repository, repositoryLink: string) => {
|
||||
if (repository._links["changesets"]) {
|
||||
return (
|
||||
<RepositoryEntryLink
|
||||
iconClass="fa-code-branch fa-lg"
|
||||
iconClass="fas fa-exchange-alt fa-lg"
|
||||
to={repositoryLink + "/changesets"}
|
||||
/>
|
||||
);
|
||||
@@ -102,6 +114,7 @@ class RepositoryEntry extends React.Component<Props> {
|
||||
</div>
|
||||
<nav className="level is-mobile">
|
||||
<div className="level-left">
|
||||
{this.renderBranchesLink(repository, repositoryLink)}
|
||||
{this.renderChangesetsLink(repository, repositoryLink)}
|
||||
{this.renderSourcesLink(repository, repositoryLink)}
|
||||
{this.renderModifyLink(repository, repositoryLink)}
|
||||
|
||||
@@ -22,9 +22,11 @@ import {
|
||||
getPageFromMatch,
|
||||
LinkPaginator,
|
||||
ChangesetList,
|
||||
Loading
|
||||
Loading,
|
||||
Notification
|
||||
} from "@scm-manager/ui-components";
|
||||
import { compose } from "redux";
|
||||
import { translate } from "react-i18next";
|
||||
|
||||
type Props = {
|
||||
repository: Repository,
|
||||
@@ -41,7 +43,8 @@ type Props = {
|
||||
fetchChangesets: (Repository, Branch, number) => void,
|
||||
|
||||
// context props
|
||||
match: any
|
||||
match: any,
|
||||
t: string => string
|
||||
};
|
||||
|
||||
class Changesets extends React.Component<Props> {
|
||||
@@ -52,7 +55,7 @@ class Changesets extends React.Component<Props> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { changesets, loading, error } = this.props;
|
||||
const { changesets, loading, error, t } = this.props;
|
||||
|
||||
if (error) {
|
||||
return <ErrorNotification error={error} />;
|
||||
@@ -63,7 +66,13 @@ class Changesets extends React.Component<Props> {
|
||||
}
|
||||
|
||||
if (!changesets || changesets.length === 0) {
|
||||
return null;
|
||||
return (
|
||||
<div className="panel-block">
|
||||
<Notification type="info">
|
||||
{t("changesets.noChangesets")}
|
||||
</Notification>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
@@ -115,6 +124,7 @@ const mapStateToProps = (state: any, ownProps: Props) => {
|
||||
};
|
||||
|
||||
export default compose(
|
||||
translate("repos"),
|
||||
withRouter,
|
||||
connect(
|
||||
mapStateToProps,
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
getBranches,
|
||||
getFetchBranchesFailure,
|
||||
isFetchBranchesPending
|
||||
} from "../modules/branches";
|
||||
} from "../branches/modules/branches";
|
||||
import { compose } from "redux";
|
||||
|
||||
type Props = {
|
||||
@@ -40,7 +40,7 @@ type Props = {
|
||||
t: string => string
|
||||
};
|
||||
|
||||
class BranchRoot extends React.Component<Props> {
|
||||
class ChangesetsRoot extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
this.props.fetchBranches(this.props.repository);
|
||||
}
|
||||
@@ -146,4 +146,4 @@ export default compose(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)
|
||||
)(BranchRoot);
|
||||
)(ChangesetsRoot);
|
||||
|
||||
@@ -74,6 +74,7 @@ class DeleteRepo extends React.Component<Props> {
|
||||
|
||||
return (
|
||||
<>
|
||||
<hr />
|
||||
<Subtitle subtitle={t("deleteRepo.subtitle")} />
|
||||
<ErrorNotification error={error} />
|
||||
<div className="columns">
|
||||
|
||||
@@ -70,7 +70,6 @@ class EditRepo extends React.Component<Props> {
|
||||
this.props.modifyRepo(repo, this.repoModified);
|
||||
}}
|
||||
/>
|
||||
<hr />
|
||||
<ExtensionPoint
|
||||
name="repo-config.route"
|
||||
props={extensionProps}
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
PageActions,
|
||||
Button,
|
||||
CreateButton,
|
||||
Notification,
|
||||
LinkPaginator,
|
||||
getPageFromMatch
|
||||
} from "@scm-manager/ui-components";
|
||||
@@ -97,15 +98,16 @@ class Overview extends React.Component<Props, State> {
|
||||
history.push("/repos/?q=" + filter);
|
||||
}}
|
||||
>
|
||||
{this.renderList()}
|
||||
{this.renderOverview()}
|
||||
{this.renderPageActionCreateButton()}
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
renderList() {
|
||||
const { collection, page } = this.props;
|
||||
if (collection) {
|
||||
renderRepositoryList() {
|
||||
const { collection, page, t } = this.props;
|
||||
|
||||
if (collection._embedded && collection._embedded.repositories.length > 0) {
|
||||
return (
|
||||
<>
|
||||
<RepositoryList repositories={collection._embedded.repositories} />
|
||||
@@ -114,6 +116,20 @@ class Overview extends React.Component<Props, State> {
|
||||
page={page}
|
||||
filter={this.getQueryString()}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Notification type="info">{t("overview.noRepositories")}</Notification>
|
||||
);
|
||||
}
|
||||
|
||||
renderOverview() {
|
||||
const { collection } = this.props;
|
||||
if (collection) {
|
||||
return (
|
||||
<>
|
||||
{this.renderRepositoryList()}
|
||||
{this.renderCreateButton()}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -12,7 +12,6 @@ import { Redirect, Route, Switch } from "react-router-dom";
|
||||
import type { Repository } from "@scm-manager/ui-types";
|
||||
|
||||
import {
|
||||
CollapsibleErrorPage,
|
||||
Loading,
|
||||
Navigation,
|
||||
SubNavigation,
|
||||
@@ -24,12 +23,14 @@ import {
|
||||
import { translate } from "react-i18next";
|
||||
import RepositoryDetails from "../components/RepositoryDetails";
|
||||
import EditRepo from "./EditRepo";
|
||||
import BranchesOverview from "../branches/containers/BranchesOverview";
|
||||
import CreateBranch from "../branches/containers/CreateBranch";
|
||||
import Permissions from "../permissions/containers/Permissions";
|
||||
|
||||
import type { History } from "history";
|
||||
import EditRepoNavLink from "../components/EditRepoNavLink";
|
||||
|
||||
import BranchRoot from "./ChangesetsRoot";
|
||||
import BranchRoot from "../branches/containers/BranchRoot";
|
||||
import ChangesetsRoot from "./ChangesetsRoot";
|
||||
import ChangesetView from "./ChangesetView";
|
||||
import PermissionsNavLink from "../components/PermissionsNavLink";
|
||||
import Sources from "../sources/containers/Sources";
|
||||
@@ -73,9 +74,15 @@ class RepositoryRoot extends React.Component<Props> {
|
||||
return this.stripEndingSlash(this.props.match.url);
|
||||
};
|
||||
|
||||
matches = (route: any) => {
|
||||
matchesBranches = (route: any) => {
|
||||
const url = this.matchedUrl();
|
||||
const regex = new RegExp(`${url}(/branches)?/?[^/]*/changesets?.*`);
|
||||
const regex = new RegExp(`${url}/branch/.+/info`);
|
||||
return route.location.pathname.match(regex);
|
||||
};
|
||||
|
||||
matchesChangesets = (route: any) => {
|
||||
const url = this.matchedUrl();
|
||||
const regex = new RegExp(`${url}(/branch)?/?[^/]*/changesets?.*`);
|
||||
return route.location.pathname.match(regex);
|
||||
};
|
||||
|
||||
@@ -160,23 +167,46 @@ class RepositoryRoot extends React.Component<Props> {
|
||||
<Route
|
||||
path={`${url}/changesets`}
|
||||
render={() => (
|
||||
<BranchRoot
|
||||
<ChangesetsRoot
|
||||
repository={repository}
|
||||
baseUrlWithBranch={`${url}/branches`}
|
||||
baseUrlWithBranch={`${url}/branch`}
|
||||
baseUrlWithoutBranch={`${url}/changesets`}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path={`${url}/branches/:branch/changesets`}
|
||||
path={`${url}/branch/:branch/changesets`}
|
||||
render={() => (
|
||||
<BranchRoot
|
||||
<ChangesetsRoot
|
||||
repository={repository}
|
||||
baseUrlWithBranch={`${url}/branches`}
|
||||
baseUrlWithBranch={`${url}/branch`}
|
||||
baseUrlWithoutBranch={`${url}/changesets`}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path={`${url}/branch/:branch`}
|
||||
render={() => (
|
||||
<BranchRoot
|
||||
repository={repository}
|
||||
baseUrl={`${url}/branch`}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path={`${url}/branches`}
|
||||
exact={true}
|
||||
render={() => (
|
||||
<BranchesOverview
|
||||
repository={repository}
|
||||
baseUrl={`${url}/branch`}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path={`${url}/branches/create`}
|
||||
render={() => <CreateBranch repository={repository} />}
|
||||
/>
|
||||
<ExtensionPoint
|
||||
name="repository.route"
|
||||
props={extensionProps}
|
||||
@@ -197,13 +227,22 @@ class RepositoryRoot extends React.Component<Props> {
|
||||
icon="fas fa-info-circle"
|
||||
label={t("repositoryRoot.menu.informationNavLink")}
|
||||
/>
|
||||
<RepositoryNavLink
|
||||
repository={repository}
|
||||
linkName="branches"
|
||||
to={`${url}/branches/`}
|
||||
icon="fas fa-code-branch"
|
||||
label={t("repositoryRoot.menu.branchesNavLink")}
|
||||
activeWhenMatch={this.matchesBranches}
|
||||
activeOnlyWhenExact={false}
|
||||
/>
|
||||
<RepositoryNavLink
|
||||
repository={repository}
|
||||
linkName="changesets"
|
||||
to={`${url}/changesets/`}
|
||||
icon="fas fa-code-branch"
|
||||
icon="fas fa-exchange-alt"
|
||||
label={t("repositoryRoot.menu.historyNavLink")}
|
||||
activeWhenMatch={this.matches}
|
||||
activeWhenMatch={this.matchesChangesets}
|
||||
activeOnlyWhenExact={false}
|
||||
/>
|
||||
<RepositoryNavLink
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
// @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}`;
|
||||
}
|
||||
@@ -1,195 +0,0 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,11 @@ import { connect } from "react-redux";
|
||||
import injectSheet from "react-jss";
|
||||
import FileTreeLeaf from "./FileTreeLeaf";
|
||||
import type { Repository, File } from "@scm-manager/ui-types";
|
||||
import { ErrorNotification, Loading } from "@scm-manager/ui-components";
|
||||
import {
|
||||
ErrorNotification,
|
||||
Loading,
|
||||
Notification
|
||||
} from "@scm-manager/ui-components";
|
||||
import {
|
||||
getFetchSourcesFailure,
|
||||
isFetchSourcesPending,
|
||||
@@ -48,16 +52,34 @@ export function findParent(path: string) {
|
||||
|
||||
class FileTree extends React.Component<Props> {
|
||||
render() {
|
||||
const {
|
||||
error,
|
||||
loading,
|
||||
tree,
|
||||
revision,
|
||||
path,
|
||||
baseUrl,
|
||||
classes,
|
||||
t
|
||||
} = this.props;
|
||||
const { error, loading, tree } = this.props;
|
||||
|
||||
if (error) {
|
||||
return <ErrorNotification error={error} />;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
if (!tree) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <div className="panel-block">{this.renderSourcesTable()}</div>;
|
||||
}
|
||||
|
||||
renderSourcesTable() {
|
||||
const { tree, revision, path, baseUrl, classes, t } = this.props;
|
||||
|
||||
const files = [];
|
||||
|
||||
if (path) {
|
||||
files.push({
|
||||
name: "..",
|
||||
path: findParent(path),
|
||||
directory: true
|
||||
});
|
||||
}
|
||||
|
||||
const compareFiles = function(f1: File, f2: File): number {
|
||||
if (f1.directory) {
|
||||
@@ -75,40 +97,19 @@ class FileTree extends React.Component<Props> {
|
||||
}
|
||||
};
|
||||
|
||||
if (error) {
|
||||
return <ErrorNotification error={error} />;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
if (!tree) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const files = [];
|
||||
|
||||
if (path) {
|
||||
files.push({
|
||||
name: "..",
|
||||
path: findParent(path),
|
||||
directory: true
|
||||
});
|
||||
}
|
||||
|
||||
if (tree._embedded && tree._embedded.children) {
|
||||
files.push(...tree._embedded.children.sort(compareFiles));
|
||||
}
|
||||
|
||||
let baseUrlWithRevision = baseUrl;
|
||||
if (revision) {
|
||||
baseUrlWithRevision += "/" + encodeURIComponent(revision);
|
||||
} else {
|
||||
baseUrlWithRevision += "/" + encodeURIComponent(tree.revision);
|
||||
}
|
||||
if (files && files.length > 0) {
|
||||
let baseUrlWithRevision = baseUrl;
|
||||
if (revision) {
|
||||
baseUrlWithRevision += "/" + encodeURIComponent(revision);
|
||||
} else {
|
||||
baseUrlWithRevision += "/" + encodeURIComponent(tree.revision);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="panel-block">
|
||||
return (
|
||||
<table className="table table-hover table-sm is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -135,8 +136,9 @@ class FileTree extends React.Component<Props> {
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
}
|
||||
return <Notification type="info">{t("sources.noSources")}</Notification>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
getBranches,
|
||||
getFetchBranchesFailure,
|
||||
isFetchBranchesPending
|
||||
} from "../../modules/branches";
|
||||
} from "../../branches/modules/branches";
|
||||
import { compose } from "redux";
|
||||
import Content from "./Content";
|
||||
import { fetchSources, isDirectory } from "../modules/sources";
|
||||
@@ -94,9 +94,7 @@ class Sources extends React.Component<Props> {
|
||||
if (currentFileIsDirectory) {
|
||||
return (
|
||||
<div className="panel">
|
||||
<div className="panel-heading">
|
||||
{this.renderBranchSelector()}
|
||||
</div>
|
||||
<div className="panel-heading">{this.renderBranchSelector()}</div>
|
||||
<FileTree
|
||||
repository={repository}
|
||||
revision={revision}
|
||||
|
||||
@@ -20,7 +20,7 @@ export default class UserRow extends React.Component<Props> {
|
||||
<td className="is-hidden-mobile">{this.renderLink(to, user.name)}</td>
|
||||
<td>{this.renderLink(to, user.displayName)}</td>
|
||||
<td>
|
||||
<a href={`mailto: ${user.mail}`}>{user.mail}</a>
|
||||
<a href={`mailto:${user.mail}`}>{user.mail}</a>
|
||||
</td>
|
||||
<td className="is-hidden-mobile">
|
||||
<input type="checkbox" id="active" checked={user.active} readOnly />
|
||||
|
||||
@@ -9,8 +9,6 @@ type Props = {
|
||||
users: User[]
|
||||
};
|
||||
|
||||
;
|
||||
|
||||
class UserTable extends React.Component<Props> {
|
||||
render() {
|
||||
const { users, t } = this.props;
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
PageActions,
|
||||
Button,
|
||||
CreateButton,
|
||||
Notification,
|
||||
LinkPaginator,
|
||||
getPageFromMatch
|
||||
} from "@scm-manager/ui-components";
|
||||
@@ -88,14 +89,26 @@ class Users extends React.Component<Props, State> {
|
||||
history.push("/users/?q=" + filter);
|
||||
}}
|
||||
>
|
||||
<UserTable users={users} />
|
||||
{this.renderPaginator()}
|
||||
{this.renderUserTable()}
|
||||
{this.renderCreateButton()}
|
||||
{this.renderPageActionCreateButton()}
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
renderUserTable() {
|
||||
const { users, t } = this.props;
|
||||
if (users && users.length > 0) {
|
||||
return (
|
||||
<>
|
||||
<UserTable users={users} />
|
||||
{this.renderPaginator()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return <Notification type="info">{t("users.noUsers")}</Notification>;
|
||||
}
|
||||
|
||||
renderPaginator = () => {
|
||||
const { list, page } = this.props;
|
||||
if (list) {
|
||||
|
||||
@@ -698,9 +698,9 @@
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85"
|
||||
|
||||
"@scm-manager/ui-bundler@^0.0.26":
|
||||
version "0.0.26"
|
||||
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.26.tgz#4676a7079b781b33fa1989c6643205c3559b1f66"
|
||||
"@scm-manager/ui-bundler@^0.0.27":
|
||||
version "0.0.27"
|
||||
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.27.tgz#3ed2c7826780b9a1a9ea90464332640cfb5d54b5"
|
||||
dependencies:
|
||||
"@babel/core" "^7.0.0"
|
||||
"@babel/plugin-proposal-class-properties" "^7.0.0"
|
||||
@@ -5669,6 +5669,10 @@ memoize-one@^4.0.0:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-4.0.3.tgz#cdfdd942853f1a1b4c71c5336b8c49da0bf0273c"
|
||||
|
||||
memoize-one@^5.0.4:
|
||||
version "5.0.4"
|
||||
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.0.4.tgz#005928aced5c43d890a4dfab18ca908b0ec92cbc"
|
||||
|
||||
memoizee@0.4.X:
|
||||
version "0.4.14"
|
||||
resolved "https://registry.yarnpkg.com/memoizee/-/memoizee-0.4.14.tgz#07a00f204699f9a95c2d9e77218271c7cd610d57"
|
||||
|
||||
@@ -16,6 +16,8 @@ import javax.ws.rs.*;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.net.URI;
|
||||
import java.util.Optional;
|
||||
|
||||
@Path(AuthenticationResource.PATH)
|
||||
@AllowAnonymousAccess
|
||||
@@ -28,6 +30,9 @@ public class AuthenticationResource {
|
||||
private final AccessTokenBuilderFactory tokenBuilderFactory;
|
||||
private final AccessTokenCookieIssuer cookieIssuer;
|
||||
|
||||
@Inject(optional = true)
|
||||
private LogoutRedirection logoutRedirection;
|
||||
|
||||
@Inject
|
||||
public AuthenticationResource(AccessTokenBuilderFactory tokenBuilderFactory, AccessTokenCookieIssuer cookieIssuer)
|
||||
{
|
||||
@@ -35,7 +40,6 @@ public class AuthenticationResource {
|
||||
this.cookieIssuer = cookieIssuer;
|
||||
}
|
||||
|
||||
|
||||
@POST
|
||||
@Path("access_token")
|
||||
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
@@ -121,6 +125,7 @@ public class AuthenticationResource {
|
||||
|
||||
@DELETE
|
||||
@Path("access_token")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 204, condition = "success"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
@@ -135,7 +140,19 @@ public class AuthenticationResource {
|
||||
cookieIssuer.invalidate(request, response);
|
||||
|
||||
// TODO anonymous access ??
|
||||
return Response.noContent().build();
|
||||
if (logoutRedirection == null) {
|
||||
return Response.noContent().build();
|
||||
} else {
|
||||
Optional<URI> uri = logoutRedirection.afterLogoutRedirectTo();
|
||||
if (uri.isPresent()) {
|
||||
return Response.ok(new RedirectAfterLogoutDto(uri.get().toASCIIString())).build();
|
||||
} else {
|
||||
return Response.noContent().build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void setLogoutRedirection(LogoutRedirection logoutRedirection) {
|
||||
this.logoutRedirection = logoutRedirection;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,15 +79,16 @@ public class BranchRootResource {
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("branch") String branchName) throws IOException {
|
||||
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
|
||||
NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name);
|
||||
try (RepositoryService repositoryService = serviceFactory.create(namespaceAndName)) {
|
||||
Branches branches = repositoryService.getBranchesCommand().getBranches();
|
||||
return branches.getBranches()
|
||||
.stream()
|
||||
.filter(branch -> branchName.equals(branch.getName()))
|
||||
.findFirst()
|
||||
.map(branch -> branchToDtoMapper.map(branch, new NamespaceAndName(namespace, name)))
|
||||
.map(branch -> branchToDtoMapper.map(branch, namespaceAndName))
|
||||
.map(Response::ok)
|
||||
.orElse(Response.status(Response.Status.NOT_FOUND))
|
||||
.orElseThrow(() -> notFound(entity("branch", branchName).in(namespaceAndName)))
|
||||
.build();
|
||||
} catch (CommandNotSupportedException ex) {
|
||||
return Response.status(Response.Status.BAD_REQUEST).build();
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public class RedirectAfterLogoutDto {
|
||||
private String logoutRedirect;
|
||||
}
|
||||
@@ -23,10 +23,18 @@ import sonia.scm.security.DefaultAccessTokenCookieIssuer;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Date;
|
||||
import java.util.Optional;
|
||||
|
||||
import static java.net.URI.create;
|
||||
import static java.util.Optional.empty;
|
||||
import static java.util.Optional.of;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@@ -49,6 +57,8 @@ public class AuthenticationResourceTest {
|
||||
|
||||
private AccessTokenCookieIssuer cookieIssuer = new DefaultAccessTokenCookieIssuer(mock(ScmConfiguration.class));
|
||||
|
||||
private MockHttpResponse response = new MockHttpResponse();
|
||||
|
||||
private static final String AUTH_JSON_TRILLIAN = "{\n" +
|
||||
"\t\"cookie\": true,\n" +
|
||||
"\t\"grant_type\": \"password\",\n" +
|
||||
@@ -101,9 +111,11 @@ public class AuthenticationResourceTest {
|
||||
"}"
|
||||
);
|
||||
|
||||
private AuthenticationResource authenticationResource;
|
||||
|
||||
@Before
|
||||
public void prepareEnvironment() {
|
||||
AuthenticationResource authenticationResource = new AuthenticationResource(accessTokenBuilderFactory, cookieIssuer);
|
||||
authenticationResource = new AuthenticationResource(accessTokenBuilderFactory, cookieIssuer);
|
||||
dispatcher.getRegistry().addSingletonResource(authenticationResource);
|
||||
|
||||
AccessToken accessToken = mock(AccessToken.class);
|
||||
@@ -123,7 +135,6 @@ public class AuthenticationResourceTest {
|
||||
public void shouldAuthCorrectly() throws URISyntaxException {
|
||||
|
||||
MockHttpRequest request = getMockHttpRequest(AUTH_JSON_TRILLIAN);
|
||||
MockHttpResponse response = new MockHttpResponse();
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
@@ -134,7 +145,6 @@ public class AuthenticationResourceTest {
|
||||
public void shouldAuthCorrectlyWithFormencodedData() throws URISyntaxException {
|
||||
|
||||
MockHttpRequest request = getMockHttpRequestUrlEncoded(AUTH_FORMENCODED_TRILLIAN);
|
||||
MockHttpResponse response = new MockHttpResponse();
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
@@ -146,7 +156,6 @@ public class AuthenticationResourceTest {
|
||||
public void shouldNotAuthUserWithWrongPassword() throws URISyntaxException {
|
||||
|
||||
MockHttpRequest request = getMockHttpRequest(AUTH_JSON_TRILLIAN_WRONG_PW);
|
||||
MockHttpResponse response = new MockHttpResponse();
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
@@ -156,7 +165,6 @@ public class AuthenticationResourceTest {
|
||||
@Test
|
||||
public void shouldNotAuthNonexistingUser() throws URISyntaxException {
|
||||
MockHttpRequest request = getMockHttpRequest(AUTH_JSON_NOT_EXISTING_USER);
|
||||
MockHttpResponse response = new MockHttpResponse();
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
@@ -187,16 +195,36 @@ public class AuthenticationResourceTest {
|
||||
@SubjectAware(username = "trillian", password = "secret")
|
||||
public void shouldSuccessfullyLogoutUser() throws URISyntaxException {
|
||||
MockHttpRequest request = MockHttpRequest.delete("/" + AuthenticationResource.PATH + "/access_token");
|
||||
MockHttpResponse response = new MockHttpResponse();
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldHandleLogoutRedirection() throws URISyntaxException, UnsupportedEncodingException {
|
||||
authenticationResource.setLogoutRedirection(() -> of(create("http://example.com/cas/logout")));
|
||||
|
||||
MockHttpRequest request = MockHttpRequest.delete("/" + AuthenticationResource.PATH + "/access_token");
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertEquals(HttpServletResponse.SC_OK, response.getStatus());
|
||||
assertThat(response.getContentAsString(), containsString("http://example.com/cas/logout"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldHandleDisabledLogoutRedirection() throws URISyntaxException {
|
||||
authenticationResource.setLogoutRedirection(Optional::empty);
|
||||
|
||||
MockHttpRequest request = MockHttpRequest.delete("/" + AuthenticationResource.PATH + "/access_token");
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus());
|
||||
}
|
||||
|
||||
private void shouldReturnBadRequest(String requestBody) throws URISyntaxException {
|
||||
MockHttpRequest request = getMockHttpRequest(requestBody);
|
||||
MockHttpResponse response = new MockHttpResponse();
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
@@ -218,5 +246,4 @@ public class AuthenticationResourceTest {
|
||||
request.contentType(MediaType.APPLICATION_FORM_URLENCODED_TYPE);
|
||||
return request;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -129,6 +129,7 @@ public class BranchRootResourceTest extends RepositoryTestBase {
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertEquals(404, response.getStatus());
|
||||
assertEquals("application/vnd.scmm-error+json;v=2", response.getOutputHeaders().getFirst("Content-Type"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user