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
|
@AfterMapping
|
||||||
void appendLinks(@MappingTarget GitRepositoryConfigDto target, @Context Repository repository) {
|
void appendLinks(@MappingTarget GitRepositoryConfigDto target, @Context Repository repository) {
|
||||||
Links.Builder linksBuilder = linkingTo().self(self());
|
Links.Builder linksBuilder = linkingTo().self(self());
|
||||||
if (RepositoryPermissions.modify(repository).isPermitted()) {
|
if (RepositoryPermissions.custom("git", repository).isPermitted()) {
|
||||||
linksBuilder.single(link("update", update()));
|
linksBuilder.single(link("update", update()));
|
||||||
}
|
}
|
||||||
target.add(linksBuilder.build());
|
target.add(linksBuilder.build());
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ public class GitRepositoryConfigResource {
|
|||||||
})
|
})
|
||||||
public Response setRepositoryConfig(@PathParam("namespace") String namespace, @PathParam("name") String name, GitRepositoryConfigDto dto) {
|
public Response setRepositoryConfig(@PathParam("namespace") String namespace, @PathParam("name") String name, GitRepositoryConfigDto dto) {
|
||||||
Repository repository = getRepository(namespace, name);
|
Repository repository = getRepository(namespace, name);
|
||||||
RepositoryPermissions.modify(repository).check();
|
RepositoryPermissions.custom("git", repository).check();
|
||||||
ConfigurationStore<GitRepositoryConfig> repositoryConfigStore = getStore(repository);
|
ConfigurationStore<GitRepositoryConfig> repositoryConfigStore = getStore(repository);
|
||||||
GitRepositoryConfig config = repositoryConfigMapper.map(dto);
|
GitRepositoryConfig config = repositoryConfigMapper.map(dto);
|
||||||
repositoryConfigStore.set(config);
|
repositoryConfigStore.set(config);
|
||||||
|
|||||||
@@ -8,11 +8,10 @@ type Props = {
|
|||||||
repository: Repository,
|
repository: Repository,
|
||||||
|
|
||||||
// context props
|
// context props
|
||||||
t: (string) => string
|
t: string => string
|
||||||
};
|
};
|
||||||
|
|
||||||
class CloneInformation extends React.Component<Props> {
|
class CloneInformation extends React.Component<Props> {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { url, repository, t } = this.props;
|
const { url, repository, t } = this.props;
|
||||||
|
|
||||||
@@ -51,7 +50,6 @@ class CloneInformation extends React.Component<Props> {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default translate("plugins")(CloneInformation);
|
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,
|
gcExpression?: string,
|
||||||
nonFastForwardDisallowed: boolean,
|
nonFastForwardDisallowed: boolean,
|
||||||
_links: Links
|
_links: Links
|
||||||
}
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
initialConfiguration: Configuration,
|
initialConfiguration: Configuration,
|
||||||
@@ -19,25 +19,24 @@ type Props = {
|
|||||||
onConfigurationChange: (Configuration, boolean) => void,
|
onConfigurationChange: (Configuration, boolean) => void,
|
||||||
|
|
||||||
// context props
|
// context props
|
||||||
t: (string) => string
|
t: string => string
|
||||||
}
|
};
|
||||||
|
|
||||||
type State = Configuration & {
|
type State = Configuration & {};
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class GitConfigurationForm extends React.Component<Props, State> {
|
class GitConfigurationForm extends React.Component<Props, State> {
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = { ...props.initialConfiguration };
|
this.state = { ...props.initialConfiguration };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
handleChange = (value: any, name: string) => {
|
handleChange = (value: any, name: string) => {
|
||||||
this.setState({
|
this.setState(
|
||||||
|
{
|
||||||
[name]: value
|
[name]: value
|
||||||
}, () => this.props.onConfigurationChange(this.state, true));
|
},
|
||||||
|
() => this.props.onConfigurationChange(this.state, true)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@@ -46,14 +45,16 @@ class GitConfigurationForm extends React.Component<Props, State> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<InputField name="gcExpression"
|
<InputField
|
||||||
|
name="gcExpression"
|
||||||
label={t("scm-git-plugin.config.gcExpression")}
|
label={t("scm-git-plugin.config.gcExpression")}
|
||||||
helpText={t("scm-git-plugin.config.gcExpressionHelpText")}
|
helpText={t("scm-git-plugin.config.gcExpressionHelpText")}
|
||||||
value={gcExpression}
|
value={gcExpression}
|
||||||
onChange={this.handleChange}
|
onChange={this.handleChange}
|
||||||
disabled={readOnly}
|
disabled={readOnly}
|
||||||
/>
|
/>
|
||||||
<Checkbox name="nonFastForwardDisallowed"
|
<Checkbox
|
||||||
|
name="nonFastForwardDisallowed"
|
||||||
label={t("scm-git-plugin.config.nonFastForwardDisallowed")}
|
label={t("scm-git-plugin.config.nonFastForwardDisallowed")}
|
||||||
helpText={t("scm-git-plugin.config.nonFastForwardDisallowedHelpText")}
|
helpText={t("scm-git-plugin.config.nonFastForwardDisallowedHelpText")}
|
||||||
checked={nonFastForwardDisallowed}
|
checked={nonFastForwardDisallowed}
|
||||||
@@ -63,7 +64,6 @@ class GitConfigurationForm extends React.Component<Props, State> {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default translate("plugins")(GitConfigurationForm);
|
export default translate("plugins")(GitConfigurationForm);
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ class RepositoryConfig extends React.Component<Props, State> {
|
|||||||
if (!(loadingBranches || loadingDefaultBranch)) {
|
if (!(loadingBranches || loadingDefaultBranch)) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<hr />
|
||||||
<Subtitle subtitle={t("scm-git-plugin.repo-config.title")}/>
|
<Subtitle subtitle={t("scm-git-plugin.repo-config.title")}/>
|
||||||
{this.renderBranchChangedNotification()}
|
{this.renderBranchChangedNotification()}
|
||||||
<form onSubmit={this.submit}>
|
<form onSubmit={this.submit}>
|
||||||
@@ -133,7 +134,6 @@ class RepositoryConfig extends React.Component<Props, State> {
|
|||||||
/>
|
/>
|
||||||
{ submitButton }
|
{ submitButton }
|
||||||
</form>
|
</form>
|
||||||
<hr />
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import GitAvatar from "./GitAvatar";
|
|||||||
|
|
||||||
import {ConfigurationBinder as cfgBinder} from "@scm-manager/ui-components";
|
import {ConfigurationBinder as cfgBinder} from "@scm-manager/ui-components";
|
||||||
import GitGlobalConfiguration from "./GitGlobalConfiguration";
|
import GitGlobalConfiguration from "./GitGlobalConfiguration";
|
||||||
|
import GitBranchInformation from "./GitBranchInformation";
|
||||||
import GitMergeInformation from "./GitMergeInformation";
|
import GitMergeInformation from "./GitMergeInformation";
|
||||||
import RepositoryConfig from "./RepositoryConfig";
|
import RepositoryConfig from "./RepositoryConfig";
|
||||||
|
|
||||||
@@ -20,6 +21,11 @@ binder.bind(
|
|||||||
ProtocolInformation,
|
ProtocolInformation,
|
||||||
gitPredicate
|
gitPredicate
|
||||||
);
|
);
|
||||||
|
binder.bind(
|
||||||
|
"repos.branch-details.information",
|
||||||
|
GitBranchInformation,
|
||||||
|
gitPredicate
|
||||||
|
);
|
||||||
binder.bind(
|
binder.bind(
|
||||||
"repos.repository-merge.information",
|
"repos.repository-merge.information",
|
||||||
GitMergeInformation,
|
GitMergeInformation,
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
{
|
{
|
||||||
"scm-git-plugin": {
|
"scm-git-plugin": {
|
||||||
"information": {
|
"information": {
|
||||||
"clone" : "Repository klonen",
|
"clone": "Repository klonen",
|
||||||
"create" : "Neues Repository erstellen",
|
"create": "Neues Repository erstellen",
|
||||||
"replace" : "Ein bestehendes Repository aktualisieren",
|
"replace": "Ein bestehendes Repository aktualisieren",
|
||||||
|
"fetch": "Remote-Änderungen herunterladen",
|
||||||
|
"checkout": "Branch wechseln",
|
||||||
"merge": {
|
"merge": {
|
||||||
"heading": "Merge des Source Branch in den Target Branch",
|
"heading": "Merge des Source Branch in den Target Branch",
|
||||||
"checkout": "1. Sicherstellen, dass der Workspace aufgeräumt ist und der Target Branch ausgecheckt wurde.",
|
"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!"
|
"success": "Der standard Branch wurde geändert!"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"permissions" : {
|
"permissions": {
|
||||||
"configuration": {
|
"configuration": {
|
||||||
"read,write": {
|
"read,write": {
|
||||||
"git": {
|
"git": {
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
"clone": "Clone the repository",
|
"clone": "Clone the repository",
|
||||||
"create": "Create a new repository",
|
"create": "Create a new repository",
|
||||||
"replace": "Push an existing repository",
|
"replace": "Push an existing repository",
|
||||||
|
"fetch": "Get remote changes",
|
||||||
|
"checkout": "Switch branch",
|
||||||
"merge": {
|
"merge": {
|
||||||
"heading": "How to merge source branch into target branch",
|
"heading": "How to merge source branch into target branch",
|
||||||
"checkout": "1. Make sure your workspace is clean and checkout 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
|
readerWriter = configuration:*:git,repository:*:id
|
||||||
admin = *
|
admin = *
|
||||||
repoRead = repository:read:*
|
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 HgAvatar from "./HgAvatar";
|
||||||
import { ConfigurationBinder as cfgBinder } from "@scm-manager/ui-components";
|
import { ConfigurationBinder as cfgBinder } from "@scm-manager/ui-components";
|
||||||
import HgGlobalConfiguration from "./HgGlobalConfiguration";
|
import HgGlobalConfiguration from "./HgGlobalConfiguration";
|
||||||
|
import HgBranchInformation from "./HgBranchInformation";
|
||||||
|
|
||||||
const hgPredicate = (props: Object) => {
|
const hgPredicate = (props: Object) => {
|
||||||
return props.repository && props.repository.type === "hg";
|
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);
|
binder.bind("repos.repository-avatar", HgAvatar, hgPredicate);
|
||||||
|
|
||||||
// bind global configuration
|
// 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": {
|
"information": {
|
||||||
"clone" : "Repository klonen",
|
"clone" : "Repository klonen",
|
||||||
"create" : "Neues Repository erstellen",
|
"create" : "Neues Repository erstellen",
|
||||||
"replace" : "Ein bestehendes Repository aktualisieren"
|
"replace" : "Ein bestehendes Repository aktualisieren",
|
||||||
|
"fetch": "Remote-Änderungen herunterladen",
|
||||||
|
"checkout": "Branch wechseln"
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"link": "Mercurial",
|
"link": "Mercurial",
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
"information": {
|
"information": {
|
||||||
"clone" : "Clone the repository",
|
"clone" : "Clone the repository",
|
||||||
"create" : "Create a new repository",
|
"create" : "Create a new repository",
|
||||||
"replace" : "Push an existing repository"
|
"replace" : "Push an existing repository",
|
||||||
|
"fetch": "Get remote changes",
|
||||||
|
"checkout": "Switch branch"
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"link": "Mercurial",
|
"link": "Mercurial",
|
||||||
|
|||||||
@@ -30,9 +30,9 @@
|
|||||||
"@scm-manager/ui-types": "2.0.0-SNAPSHOT",
|
"@scm-manager/ui-types": "2.0.0-SNAPSHOT",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"moment": "^2.22.2",
|
"moment": "^2.22.2",
|
||||||
"react": "^16.5.2",
|
"react": "^16.8.6",
|
||||||
|
"react-dom": "^16.8.6",
|
||||||
"react-diff-view": "^1.8.1",
|
"react-diff-view": "^1.8.1",
|
||||||
"react-dom": "^16.5.2",
|
|
||||||
"react-i18next": "^7.11.0",
|
"react-i18next": "^7.11.0",
|
||||||
"react-jss": "^8.6.1",
|
"react-jss": "^8.6.1",
|
||||||
"react-router-dom": "^4.3.1",
|
"react-router-dom": "^4.3.1",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class MailLink extends React.Component<Props> {
|
|||||||
if (!address) {
|
if (!address) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return <a href={"mailto: " + address}>{address}</a>;
|
return <a href={"mailto:" + address}>{address}</a>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ type Props = {
|
|||||||
value?: string,
|
value?: string,
|
||||||
onChange: (value: string, name?: string) => void,
|
onChange: (value: string, name?: string) => void,
|
||||||
loading?: boolean,
|
loading?: boolean,
|
||||||
helpText?: string
|
helpText?: string,
|
||||||
|
disabled?: boolean
|
||||||
};
|
};
|
||||||
|
|
||||||
class Select extends React.Component<Props> {
|
class Select extends React.Component<Props> {
|
||||||
@@ -34,7 +35,7 @@ class Select extends React.Component<Props> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { options, value, label, helpText, loading } = this.props;
|
const { options, value, label, helpText, loading, disabled } = this.props;
|
||||||
const loadingClass = loading ? "is-loading" : "";
|
const loadingClass = loading ? "is-loading" : "";
|
||||||
|
|
||||||
|
|
||||||
@@ -51,6 +52,7 @@ class Select extends React.Component<Props> {
|
|||||||
}}
|
}}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={this.handleInput}
|
onChange={this.handleInput}
|
||||||
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
{options.map(opt => {
|
{options.map(opt => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
import injectSheets from "react-jss";
|
import injectSheets from "react-jss";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { translate } from "react-i18next";
|
import { translate } from "react-i18next";
|
||||||
|
import {Button} from "../buttons";
|
||||||
|
|
||||||
const styles = {
|
const styles = {
|
||||||
panel: {
|
panel: {
|
||||||
@@ -39,14 +40,16 @@ type Props = DiffObjectProps & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
collapsed: boolean
|
collapsed: boolean,
|
||||||
|
sideBySide: boolean
|
||||||
};
|
};
|
||||||
|
|
||||||
class DiffFile extends React.Component<Props, State> {
|
class DiffFile extends React.Component<Props, State> {
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
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) => {
|
setCollapse = (collapsed: boolean) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
collapsed
|
collapsed
|
||||||
@@ -149,10 +158,10 @@ class DiffFile extends React.Component<Props, State> {
|
|||||||
file,
|
file,
|
||||||
fileControlFactory,
|
fileControlFactory,
|
||||||
fileAnnotationFactory,
|
fileAnnotationFactory,
|
||||||
sideBySide,
|
classes,
|
||||||
classes
|
t
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { collapsed } = this.state;
|
const { collapsed, sideBySide } = this.state;
|
||||||
const viewType = sideBySide ? "split" : "unified";
|
const viewType = sideBySide ? "split" : "unified";
|
||||||
|
|
||||||
let body = null;
|
let body = null;
|
||||||
@@ -173,14 +182,10 @@ class DiffFile extends React.Component<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fileControls = fileControlFactory ? fileControlFactory(file, this.setCollapse) : null;
|
const fileControls = fileControlFactory ? fileControlFactory(file, this.setCollapse) : null;
|
||||||
return (
|
return <div className={classNames("panel", classes.panel)}>
|
||||||
<div className={classNames("panel", classes.panel)}>
|
|
||||||
<div className="panel-heading">
|
<div className="panel-heading">
|
||||||
<div className="level">
|
<div className="level">
|
||||||
<div
|
<div className={classNames("level-left", classes.titleHeader)} onClick={this.toggleCollapse}>
|
||||||
className={classNames("level-left", classes.titleHeader)}
|
|
||||||
onClick={this.toggleCollapse}
|
|
||||||
>
|
|
||||||
<i className={icon} />
|
<i className={icon} />
|
||||||
<span className={classes.title}>
|
<span className={classes.title}>
|
||||||
{this.renderFileTitle(file)}
|
{this.renderFileTitle(file)}
|
||||||
@@ -189,12 +194,21 @@ class DiffFile extends React.Component<Props, State> {
|
|||||||
{this.renderChangeTag(file)}
|
{this.renderChangeTag(file)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
{body}
|
{body}
|
||||||
</div>
|
</div>;
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import type { Changeset } from "@scm-manager/ui-types";
|
import type { Changeset } from "@scm-manager/ui-types";
|
||||||
import { ExtensionPoint } from "@scm-manager/ui-extensions";
|
import { ExtensionPoint } from "@scm-manager/ui-extensions";
|
||||||
import {translate} from "react-i18next";
|
import { translate } from "react-i18next";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
changeset: Changeset,
|
changeset: Changeset,
|
||||||
|
|
||||||
// context props
|
// context props
|
||||||
t: (string) => string
|
t: string => string
|
||||||
};
|
};
|
||||||
|
|
||||||
class ChangesetAuthor extends React.Component<Props> {
|
class ChangesetAuthor extends React.Component<Props> {
|
||||||
@@ -28,7 +28,10 @@ class ChangesetAuthor extends React.Component<Props> {
|
|||||||
renderWithMail(name: string, mail: string) {
|
renderWithMail(name: string, mail: string) {
|
||||||
const { t } = this.props;
|
const { t } = this.props;
|
||||||
return (
|
return (
|
||||||
<a href={"mailto: " + mail} title={t("changeset.author.mailto") + " " + mail}>
|
<a
|
||||||
|
href={"mailto:" + mail}
|
||||||
|
title={t("changeset.author.mailto") + " " + mail}
|
||||||
|
>
|
||||||
{name}
|
{name}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,11 +11,10 @@ type Props = {
|
|||||||
changeset: Changeset,
|
changeset: Changeset,
|
||||||
|
|
||||||
// context props
|
// context props
|
||||||
t: (string) => string
|
t: string => string
|
||||||
}
|
};
|
||||||
|
|
||||||
class ChangesetButtonGroup extends React.Component<Props> {
|
class ChangesetButtonGroup extends React.Component<Props> {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { repository, changeset, t } = this.props;
|
const { repository, changeset, t } = this.props;
|
||||||
|
|
||||||
@@ -26,7 +25,7 @@ class ChangesetButtonGroup extends React.Component<Props> {
|
|||||||
<ButtonGroup className="is-pulled-right">
|
<ButtonGroup className="is-pulled-right">
|
||||||
<Button link={changesetLink}>
|
<Button link={changesetLink}>
|
||||||
<span className="icon">
|
<span className="icon">
|
||||||
<i className="fas fa-code-branch"></i>
|
<i className="fas fa-exchange-alt" />
|
||||||
</span>
|
</span>
|
||||||
<span className="is-hidden-mobile is-hidden-tablet-only">
|
<span className="is-hidden-mobile is-hidden-tablet-only">
|
||||||
{t("changeset.buttons.details")}
|
{t("changeset.buttons.details")}
|
||||||
@@ -34,7 +33,7 @@ class ChangesetButtonGroup extends React.Component<Props> {
|
|||||||
</Button>
|
</Button>
|
||||||
<Button link={sourcesLink}>
|
<Button link={sourcesLink}>
|
||||||
<span className="icon">
|
<span className="icon">
|
||||||
<i className="fas fa-code"></i>
|
<i className="fas fa-code" />
|
||||||
</span>
|
</span>
|
||||||
<span className="is-hidden-mobile is-hidden-tablet-only">
|
<span className="is-hidden-mobile is-hidden-tablet-only">
|
||||||
{t("changeset.buttons.sources")}
|
{t("changeset.buttons.sources")}
|
||||||
@@ -43,7 +42,6 @@ class ChangesetButtonGroup extends React.Component<Props> {
|
|||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default translate("repos")(ChangesetButtonGroup);
|
export default translate("repos")(ChangesetButtonGroup);
|
||||||
|
|||||||
@@ -6623,14 +6623,14 @@ react-dom@^16.4.2:
|
|||||||
prop-types "^15.6.2"
|
prop-types "^15.6.2"
|
||||||
scheduler "^0.10.0"
|
scheduler "^0.10.0"
|
||||||
|
|
||||||
react-dom@^16.5.2:
|
react-dom@^16.8.6:
|
||||||
version "16.5.2"
|
version "16.8.6"
|
||||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.5.2.tgz#b69ee47aa20bab5327b2b9d7c1fe2a30f2cfa9d7"
|
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.6.tgz#71d6303f631e8b0097f56165ef608f051ff6e10f"
|
||||||
dependencies:
|
dependencies:
|
||||||
loose-envify "^1.1.0"
|
loose-envify "^1.1.0"
|
||||||
object-assign "^4.1.1"
|
object-assign "^4.1.1"
|
||||||
prop-types "^15.6.2"
|
prop-types "^15.6.2"
|
||||||
schedule "^0.5.0"
|
scheduler "^0.13.6"
|
||||||
|
|
||||||
react-i18next@^7.11.0:
|
react-i18next@^7.11.0:
|
||||||
version "7.13.0"
|
version "7.13.0"
|
||||||
@@ -6755,14 +6755,14 @@ react@^16.4.2:
|
|||||||
prop-types "^15.6.2"
|
prop-types "^15.6.2"
|
||||||
scheduler "^0.10.0"
|
scheduler "^0.10.0"
|
||||||
|
|
||||||
react@^16.5.2:
|
react@^16.8.6:
|
||||||
version "16.5.2"
|
version "16.8.6"
|
||||||
resolved "https://registry.yarnpkg.com/react/-/react-16.5.2.tgz#19f6b444ed139baa45609eee6dc3d318b3895d42"
|
resolved "https://registry.yarnpkg.com/react/-/react-16.8.6.tgz#ad6c3a9614fd3a4e9ef51117f54d888da01f2bbe"
|
||||||
dependencies:
|
dependencies:
|
||||||
loose-envify "^1.1.0"
|
loose-envify "^1.1.0"
|
||||||
object-assign "^4.1.1"
|
object-assign "^4.1.1"
|
||||||
prop-types "^15.6.2"
|
prop-types "^15.6.2"
|
||||||
schedule "^0.5.0"
|
scheduler "^0.13.6"
|
||||||
|
|
||||||
read-only-stream@^2.0.0:
|
read-only-stream@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
@@ -7229,6 +7229,13 @@ scheduler@^0.10.0:
|
|||||||
loose-envify "^1.1.0"
|
loose-envify "^1.1.0"
|
||||||
object-assign "^4.1.1"
|
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:
|
select@^1.1.2:
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d"
|
resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d"
|
||||||
|
|||||||
@@ -4,5 +4,11 @@ import type {Links} from "./hal";
|
|||||||
export type Branch = {
|
export type Branch = {
|
||||||
name: string,
|
name: string,
|
||||||
revision: string,
|
revision: string,
|
||||||
|
defaultBranch?: boolean,
|
||||||
_links: Links
|
_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 { Repository, RepositoryCollection, RepositoryGroup } from "./Repositories";
|
||||||
export type { RepositoryType, RepositoryTypeCollection } from "./RepositoryTypes";
|
export type { RepositoryType, RepositoryTypeCollection } from "./RepositoryTypes";
|
||||||
|
|
||||||
export type { Branch } from "./Branches";
|
export type { Branch, BranchRequest } from "./Branches";
|
||||||
|
|
||||||
export type { Changeset } from "./Changesets";
|
export type { Changeset } from "./Changesets";
|
||||||
|
|
||||||
|
|||||||
@@ -17,11 +17,12 @@
|
|||||||
"i18next-browser-languagedetector": "^2.2.2",
|
"i18next-browser-languagedetector": "^2.2.2",
|
||||||
"i18next-fetch-backend": "^0.1.0",
|
"i18next-fetch-backend": "^0.1.0",
|
||||||
"jss-nested": "^6.0.1",
|
"jss-nested": "^6.0.1",
|
||||||
|
"memoize-one": "^5.0.4",
|
||||||
"moment": "^2.22.2",
|
"moment": "^2.22.2",
|
||||||
"node-sass": "^4.9.3",
|
"node-sass": "^4.9.3",
|
||||||
"postcss-easy-import": "^3.0.0",
|
"postcss-easy-import": "^3.0.0",
|
||||||
"react": "^16.8.6",
|
|
||||||
"query-string": "5",
|
"query-string": "5",
|
||||||
|
"react": "^16.8.6",
|
||||||
"react-diff-view": "^1.8.1",
|
"react-diff-view": "^1.8.1",
|
||||||
"react-dom": "^16.8.6",
|
"react-dom": "^16.8.6",
|
||||||
"react-i18next": "^7.9.0",
|
"react-i18next": "^7.9.0",
|
||||||
@@ -53,7 +54,7 @@
|
|||||||
"pre-commit": "jest && flow && eslint src"
|
"pre-commit": "jest && flow && eslint src"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@scm-manager/ui-bundler": "^0.0.26",
|
"@scm-manager/ui-bundler": "^0.0.27",
|
||||||
"concat": "^1.0.3",
|
"concat": "^1.0.3",
|
||||||
"copyfiles": "^2.0.0",
|
"copyfiles": "^2.0.0",
|
||||||
"enzyme": "^3.3.0",
|
"enzyme": "^3.3.0",
|
||||||
|
|||||||
@@ -10,7 +10,8 @@
|
|||||||
},
|
},
|
||||||
"groups": {
|
"groups": {
|
||||||
"title": "Gruppen",
|
"title": "Gruppen",
|
||||||
"subtitle": "Verwaltung der Gruppen"
|
"subtitle": "Verwaltung der Gruppen",
|
||||||
|
"noGroups": "Keine Gruppen gefunden."
|
||||||
},
|
},
|
||||||
"singleGroup": {
|
"singleGroup": {
|
||||||
"errorTitle": "Fehler",
|
"errorTitle": "Fehler",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
"menu": {
|
"menu": {
|
||||||
"navigationLabel": "Repository Navigation",
|
"navigationLabel": "Repository Navigation",
|
||||||
"informationNavLink": "Informationen",
|
"informationNavLink": "Informationen",
|
||||||
|
"branchesNavLink": "Branches",
|
||||||
"historyNavLink": "Commits",
|
"historyNavLink": "Commits",
|
||||||
"sourcesNavLink": "Sources",
|
"sourcesNavLink": "Sources",
|
||||||
"settingsNavLink": "Einstellungen",
|
"settingsNavLink": "Einstellungen",
|
||||||
@@ -36,15 +37,39 @@
|
|||||||
"overview": {
|
"overview": {
|
||||||
"title": "Repositories",
|
"title": "Repositories",
|
||||||
"subtitle": "Übersicht aller verfügbaren Repositories",
|
"subtitle": "Übersicht aller verfügbaren Repositories",
|
||||||
|
"noRepositories": "Keine Repositories gefunden.",
|
||||||
"createButton": "Repository erstellen"
|
"createButton": "Repository erstellen"
|
||||||
},
|
},
|
||||||
"create": {
|
"create": {
|
||||||
"title": "Repository erstellen",
|
"title": "Repository erstellen",
|
||||||
"subtitle": "Erstellen eines neuen Repository"
|
"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": {
|
"changesets": {
|
||||||
"errorTitle": "Fehler",
|
"errorTitle": "Fehler",
|
||||||
"errorSubtitle": "Changesets konnten nicht abgerufen werden",
|
"errorSubtitle": "Changesets konnten nicht abgerufen werden",
|
||||||
|
"noChangesets": "Keine Changesets in diesem Branch gefunden.",
|
||||||
"branchSelectorLabel": "Branches"
|
"branchSelectorLabel": "Branches"
|
||||||
},
|
},
|
||||||
"changeset": {
|
"changeset": {
|
||||||
@@ -83,7 +108,8 @@
|
|||||||
"lastModified": "Zuletzt bearbeitet",
|
"lastModified": "Zuletzt bearbeitet",
|
||||||
"description": "Beschreibung",
|
"description": "Beschreibung",
|
||||||
"size": "Größe"
|
"size": "Größe"
|
||||||
}
|
},
|
||||||
|
"noSources": "Keine Sources in diesem Branch gefunden."
|
||||||
},
|
},
|
||||||
"permission": {
|
"permission": {
|
||||||
"title": "Berechtigungen bearbeiten",
|
"title": "Berechtigungen bearbeiten",
|
||||||
@@ -147,5 +173,9 @@
|
|||||||
"submit": "Ja",
|
"submit": "Ja",
|
||||||
"cancel": "Nein"
|
"cancel": "Nein"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"diff": {
|
||||||
|
"sideBySide": "Zweispalitg",
|
||||||
|
"combined": "Kombiniert"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
"users": {
|
"users": {
|
||||||
"title": "Benutzer",
|
"title": "Benutzer",
|
||||||
"subtitle": "Verwaltung der Benutzer",
|
"subtitle": "Verwaltung der Benutzer",
|
||||||
|
"noUsers": "Keine Benutzer gefunden.",
|
||||||
"createButton": "Benutzer erstellen"
|
"createButton": "Benutzer erstellen"
|
||||||
},
|
},
|
||||||
"singleUser": {
|
"singleUser": {
|
||||||
|
|||||||
@@ -10,7 +10,8 @@
|
|||||||
},
|
},
|
||||||
"groups": {
|
"groups": {
|
||||||
"title": "Groups",
|
"title": "Groups",
|
||||||
"subtitle": "Create, read, update and delete groups"
|
"subtitle": "Create, read, update and delete groups",
|
||||||
|
"noGroups": "No groups found."
|
||||||
},
|
},
|
||||||
"singleGroup": {
|
"singleGroup": {
|
||||||
"errorTitle": "Error",
|
"errorTitle": "Error",
|
||||||
|
|||||||
@@ -11,7 +11,10 @@
|
|||||||
"validation": {
|
"validation": {
|
||||||
"namespace-invalid": "The repository namespace is invalid",
|
"namespace-invalid": "The repository namespace is invalid",
|
||||||
"name-invalid": "The repository name 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": {
|
"help": {
|
||||||
"namespaceHelpText": "The namespace of the repository. This name will be part of the repository url.",
|
"namespaceHelpText": "The namespace of the repository. This name will be part of the repository url.",
|
||||||
@@ -26,6 +29,7 @@
|
|||||||
"menu": {
|
"menu": {
|
||||||
"navigationLabel": "Repository Navigation",
|
"navigationLabel": "Repository Navigation",
|
||||||
"informationNavLink": "Information",
|
"informationNavLink": "Information",
|
||||||
|
"branchesNavLink": "Branches",
|
||||||
"historyNavLink": "Commits",
|
"historyNavLink": "Commits",
|
||||||
"sourcesNavLink": "Sources",
|
"sourcesNavLink": "Sources",
|
||||||
"settingsNavLink": "Settings",
|
"settingsNavLink": "Settings",
|
||||||
@@ -36,15 +40,39 @@
|
|||||||
"overview": {
|
"overview": {
|
||||||
"title": "Repositories",
|
"title": "Repositories",
|
||||||
"subtitle": "Overview of available repositories",
|
"subtitle": "Overview of available repositories",
|
||||||
|
"noRepositories": "No repositories found.",
|
||||||
"createButton": "Create Repository"
|
"createButton": "Create Repository"
|
||||||
},
|
},
|
||||||
"create": {
|
"create": {
|
||||||
"title": "Create Repository",
|
"title": "Create Repository",
|
||||||
"subtitle": "Create a new 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": {
|
"changesets": {
|
||||||
"errorTitle": "Error",
|
"errorTitle": "Error",
|
||||||
"errorSubtitle": "Could not fetch changesets",
|
"errorSubtitle": "Could not fetch changesets",
|
||||||
|
"noChangesets": "No changesets found for this branch.",
|
||||||
"branchSelectorLabel": "Branches"
|
"branchSelectorLabel": "Branches"
|
||||||
},
|
},
|
||||||
"changeset": {
|
"changeset": {
|
||||||
@@ -83,7 +111,8 @@
|
|||||||
"lastModified": "Last modified",
|
"lastModified": "Last modified",
|
||||||
"description": "Description",
|
"description": "Description",
|
||||||
"size": "Size"
|
"size": "Size"
|
||||||
}
|
},
|
||||||
|
"noSources": "No sources found for this branch."
|
||||||
},
|
},
|
||||||
"permission": {
|
"permission": {
|
||||||
"title": "Edit Permissions",
|
"title": "Edit Permissions",
|
||||||
@@ -155,6 +184,8 @@
|
|||||||
"modify": "modified",
|
"modify": "modified",
|
||||||
"rename": "renamed",
|
"rename": "renamed",
|
||||||
"copy": "copied"
|
"copy": "copied"
|
||||||
}
|
},
|
||||||
|
"sideBySide": "side-by-side",
|
||||||
|
"combined": "combined"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
"users": {
|
"users": {
|
||||||
"title": "Users",
|
"title": "Users",
|
||||||
"subtitle": "Create, read, update and delete users",
|
"subtitle": "Create, read, update and delete users",
|
||||||
|
"noUsers": "No users found.",
|
||||||
"createButton": "Create User"
|
"createButton": "Create User"
|
||||||
},
|
},
|
||||||
"singleUser": {
|
"singleUser": {
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ class Index extends Component<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { indexResources, loading, error, t } = this.props;
|
const { indexResources, loading, error } = this.props;
|
||||||
const { pluginsLoaded } = this.state;
|
const { pluginsLoaded } = this.state;
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
logout,
|
logout,
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
isLogoutPending,
|
isLogoutPending,
|
||||||
getLogoutFailure
|
getLogoutFailure, isRedirecting
|
||||||
} from "../modules/auth";
|
} from "../modules/auth";
|
||||||
import { Loading, ErrorPage } from "@scm-manager/ui-components";
|
import { Loading, ErrorPage } from "@scm-manager/ui-components";
|
||||||
import { getLogoutLink } from "../modules/indexResource";
|
import { getLogoutLink } from "../modules/indexResource";
|
||||||
@@ -16,6 +16,7 @@ import { getLogoutLink } from "../modules/indexResource";
|
|||||||
type Props = {
|
type Props = {
|
||||||
authenticated: boolean,
|
authenticated: boolean,
|
||||||
loading: boolean,
|
loading: boolean,
|
||||||
|
redirecting: boolean,
|
||||||
error: Error,
|
error: Error,
|
||||||
logoutLink: string,
|
logoutLink: string,
|
||||||
|
|
||||||
@@ -32,7 +33,7 @@ class Logout extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { authenticated, loading, error, t } = this.props;
|
const { authenticated, redirecting, loading, error, t } = this.props;
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<ErrorPage
|
<ErrorPage
|
||||||
@@ -41,7 +42,7 @@ class Logout extends React.Component<Props> {
|
|||||||
error={error}
|
error={error}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (loading || authenticated) {
|
} else if (loading || authenticated || redirecting) {
|
||||||
return <Loading />;
|
return <Loading />;
|
||||||
} else {
|
} else {
|
||||||
return <Redirect to="/login" />;
|
return <Redirect to="/login" />;
|
||||||
@@ -52,11 +53,13 @@ class Logout extends React.Component<Props> {
|
|||||||
const mapStateToProps = state => {
|
const mapStateToProps = state => {
|
||||||
const authenticated = isAuthenticated(state);
|
const authenticated = isAuthenticated(state);
|
||||||
const loading = isLogoutPending(state);
|
const loading = isLogoutPending(state);
|
||||||
|
const redirecting = isRedirecting(state);
|
||||||
const error = getLogoutFailure(state);
|
const error = getLogoutFailure(state);
|
||||||
const logoutLink = getLogoutLink(state);
|
const logoutLink = getLogoutLink(state);
|
||||||
return {
|
return {
|
||||||
authenticated,
|
authenticated,
|
||||||
loading,
|
loading,
|
||||||
|
redirecting,
|
||||||
error,
|
error,
|
||||||
logoutLink
|
logoutLink
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -61,13 +61,11 @@ class PluginLoader extends React.Component<Props, State> {
|
|||||||
}
|
}
|
||||||
return promises.reduce((chain, current) => {
|
return promises.reduce((chain, current) => {
|
||||||
return chain.then(chainResults => {
|
return chain.then(chainResults => {
|
||||||
return current.then(currentResult => [...chainResults, currentResult])
|
return current.then(currentResult => [...chainResults, currentResult]);
|
||||||
}
|
});
|
||||||
);
|
|
||||||
}, Promise.resolve([]));
|
}, Promise.resolve([]));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
loadPlugin = (plugin: Plugin) => {
|
loadPlugin = (plugin: Plugin) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
message: `loading ${plugin.name}`
|
message: `loading ${plugin.name}`
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import namespaceStrategies from "./config/modules/namespaceStrategies";
|
|||||||
import indexResources from "./modules/indexResource";
|
import indexResources from "./modules/indexResource";
|
||||||
|
|
||||||
import type { BrowserHistory } from "history/createBrowserHistory";
|
import type { BrowserHistory } from "history/createBrowserHistory";
|
||||||
import branches from "./repos/modules/branches";
|
import branches from "./repos/branches/modules/branches";
|
||||||
|
|
||||||
function createReduxStore(history: BrowserHistory) {
|
function createReduxStore(history: BrowserHistory) {
|
||||||
const composeEnhancers =
|
const composeEnhancers =
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import type { Group } from "@scm-manager/ui-types";
|
import type { Group } from "@scm-manager/ui-types";
|
||||||
import { Checkbox } from "@scm-manager/ui-components"
|
import { Checkbox } from "@scm-manager/ui-components";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
group: Group
|
group: Group
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
Page,
|
Page,
|
||||||
PageActions,
|
PageActions,
|
||||||
Button,
|
Button,
|
||||||
|
Notification,
|
||||||
LinkPaginator,
|
LinkPaginator,
|
||||||
getPageFromMatch
|
getPageFromMatch
|
||||||
} from "@scm-manager/ui-components";
|
} from "@scm-manager/ui-components";
|
||||||
@@ -64,6 +65,13 @@ class Groups extends React.Component<Props, State> {
|
|||||||
this.setState({ page: page });
|
this.setState({ page: page });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onPageChange = (link: string) => {
|
||||||
|
this.props.fetchGroupsByLink(link);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* reflect page transitions in the uri
|
||||||
|
*/
|
||||||
componentDidUpdate = (prevProps: Props) => {
|
componentDidUpdate = (prevProps: Props) => {
|
||||||
const { list, page, location, fetchGroupsByPage, groupLink } = this.props;
|
const { list, page, location, fetchGroupsByPage, groupLink } = this.props;
|
||||||
if (list && page) {
|
if (list && page) {
|
||||||
@@ -89,14 +97,26 @@ class Groups extends React.Component<Props, State> {
|
|||||||
history.push("/groups/?q=" + filter);
|
history.push("/groups/?q=" + filter);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<GroupTable groups={groups} />
|
{this.renderGroupTable()}
|
||||||
{this.renderPaginator()}
|
|
||||||
{this.renderCreateButton()}
|
{this.renderCreateButton()}
|
||||||
{this.renderPageActionCreateButton()}
|
{this.renderPageActionCreateButton()}
|
||||||
</Page>
|
</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 = () => {
|
renderPaginator = () => {
|
||||||
const { list, page } = this.props;
|
const { list, page } = this.props;
|
||||||
if (list) {
|
if (list) {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export const LOGOUT = "scm/auth/LOGOUT";
|
|||||||
export const LOGOUT_PENDING = `${LOGOUT}_${types.PENDING_SUFFIX}`;
|
export const LOGOUT_PENDING = `${LOGOUT}_${types.PENDING_SUFFIX}`;
|
||||||
export const LOGOUT_SUCCESS = `${LOGOUT}_${types.SUCCESS_SUFFIX}`;
|
export const LOGOUT_SUCCESS = `${LOGOUT}_${types.SUCCESS_SUFFIX}`;
|
||||||
export const LOGOUT_FAILURE = `${LOGOUT}_${types.FAILURE_SUFFIX}`;
|
export const LOGOUT_FAILURE = `${LOGOUT}_${types.FAILURE_SUFFIX}`;
|
||||||
|
export const LOGOUT_REDIRECT = `${LOGOUT}_REDIRECT`;
|
||||||
|
|
||||||
// Reducer
|
// Reducer
|
||||||
|
|
||||||
@@ -54,6 +55,13 @@ export default function reducer(
|
|||||||
case LOGOUT_SUCCESS:
|
case LOGOUT_SUCCESS:
|
||||||
return initialState;
|
return initialState;
|
||||||
|
|
||||||
|
case LOGOUT_REDIRECT: {
|
||||||
|
// we keep the current state until we are redirected to the new page
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
redirecting: true
|
||||||
|
};
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
@@ -89,10 +97,16 @@ export const logoutPending = () => {
|
|||||||
|
|
||||||
export const logoutSuccess = () => {
|
export const logoutSuccess = () => {
|
||||||
return {
|
return {
|
||||||
type: LOGOUT_SUCCESS
|
type: LOGOUT_SUCCESS,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const redirectAfterLogout = () => {
|
||||||
|
return {
|
||||||
|
type: LOGOUT_REDIRECT
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const logoutFailure = (error: Error) => {
|
export const logoutFailure = (error: Error) => {
|
||||||
return {
|
return {
|
||||||
type: LOGOUT_FAILURE,
|
type: LOGOUT_FAILURE,
|
||||||
@@ -130,9 +144,7 @@ export const fetchMeFailure = (error: Error) => {
|
|||||||
// side effects
|
// side effects
|
||||||
|
|
||||||
const callFetchMe = (link: string): Promise<Me> => {
|
const callFetchMe = (link: string): Promise<Me> => {
|
||||||
return apiClient
|
return apiClient.get(link).then(response => {
|
||||||
.get(link)
|
|
||||||
.then(response => {
|
|
||||||
return response.json();
|
return response.json();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -192,11 +204,28 @@ export const logout = (link: string) => {
|
|||||||
dispatch(logoutPending());
|
dispatch(logoutPending());
|
||||||
return apiClient
|
return apiClient
|
||||||
.delete(link)
|
.delete(link)
|
||||||
.then(() => {
|
.then(response => {
|
||||||
dispatch(logoutSuccess());
|
return response.status === 200
|
||||||
|
? response.json()
|
||||||
|
: new Promise(function(resolve) {
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.then(() => {
|
.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());
|
dispatch(fetchIndexResources());
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
dispatch(logoutFailure(error));
|
dispatch(logoutFailure(error));
|
||||||
@@ -244,3 +273,8 @@ export const isLogoutPending = (state: Object) => {
|
|||||||
export const getLogoutFailure = (state: Object) => {
|
export const getLogoutFailure = (state: Object) => {
|
||||||
return getFailure(state, LOGOUT);
|
return getFailure(state, LOGOUT);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isRedirecting = (state: Object) => {
|
||||||
|
return !!stateAuth(state).redirecting;
|
||||||
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import reducer, {
|
|||||||
FETCH_ME,
|
FETCH_ME,
|
||||||
LOGOUT,
|
LOGOUT,
|
||||||
getLoginFailure,
|
getLoginFailure,
|
||||||
getLogoutFailure
|
getLogoutFailure, isRedirecting, LOGOUT_REDIRECT, redirectAfterLogout,
|
||||||
} from "./auth";
|
} from "./auth";
|
||||||
|
|
||||||
import configureMockStore from "redux-mock-store";
|
import configureMockStore from "redux-mock-store";
|
||||||
@@ -70,6 +70,17 @@ describe("auth reducer", () => {
|
|||||||
expect(state.authenticated).toBeUndefined();
|
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", () => {
|
it("should set state authenticated and me after login", () => {
|
||||||
const state = reducer(undefined, loginSuccess(me));
|
const state = reducer(undefined, loginSuccess(me));
|
||||||
expect(state.me).toBe(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", () => {
|
it("should dispatch logout failure", () => {
|
||||||
fetchMock.deleteOnce("/api/v2/auth/access_token", {
|
fetchMock.deleteOnce("/api/v2/auth/access_token", {
|
||||||
status: 500
|
status: 500
|
||||||
@@ -307,4 +353,16 @@ describe("auth selectors", () => {
|
|||||||
it("should return unknown, if failure state is not set for LOGOUT", () => {
|
it("should return unknown, if failure state is not set for LOGOUT", () => {
|
||||||
expect(getLogoutFailure({})).toBeUndefined();
|
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}`;
|
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) => {
|
renderChangesetsLink = (repository: Repository, repositoryLink: string) => {
|
||||||
if (repository._links["changesets"]) {
|
if (repository._links["changesets"]) {
|
||||||
return (
|
return (
|
||||||
<RepositoryEntryLink
|
<RepositoryEntryLink
|
||||||
iconClass="fa-code-branch fa-lg"
|
iconClass="fas fa-exchange-alt fa-lg"
|
||||||
to={repositoryLink + "/changesets"}
|
to={repositoryLink + "/changesets"}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -102,6 +114,7 @@ class RepositoryEntry extends React.Component<Props> {
|
|||||||
</div>
|
</div>
|
||||||
<nav className="level is-mobile">
|
<nav className="level is-mobile">
|
||||||
<div className="level-left">
|
<div className="level-left">
|
||||||
|
{this.renderBranchesLink(repository, repositoryLink)}
|
||||||
{this.renderChangesetsLink(repository, repositoryLink)}
|
{this.renderChangesetsLink(repository, repositoryLink)}
|
||||||
{this.renderSourcesLink(repository, repositoryLink)}
|
{this.renderSourcesLink(repository, repositoryLink)}
|
||||||
{this.renderModifyLink(repository, repositoryLink)}
|
{this.renderModifyLink(repository, repositoryLink)}
|
||||||
|
|||||||
@@ -22,9 +22,11 @@ import {
|
|||||||
getPageFromMatch,
|
getPageFromMatch,
|
||||||
LinkPaginator,
|
LinkPaginator,
|
||||||
ChangesetList,
|
ChangesetList,
|
||||||
Loading
|
Loading,
|
||||||
|
Notification
|
||||||
} from "@scm-manager/ui-components";
|
} from "@scm-manager/ui-components";
|
||||||
import { compose } from "redux";
|
import { compose } from "redux";
|
||||||
|
import { translate } from "react-i18next";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
repository: Repository,
|
repository: Repository,
|
||||||
@@ -41,7 +43,8 @@ type Props = {
|
|||||||
fetchChangesets: (Repository, Branch, number) => void,
|
fetchChangesets: (Repository, Branch, number) => void,
|
||||||
|
|
||||||
// context props
|
// context props
|
||||||
match: any
|
match: any,
|
||||||
|
t: string => string
|
||||||
};
|
};
|
||||||
|
|
||||||
class Changesets extends React.Component<Props> {
|
class Changesets extends React.Component<Props> {
|
||||||
@@ -52,7 +55,7 @@ class Changesets extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { changesets, loading, error } = this.props;
|
const { changesets, loading, error, t } = this.props;
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return <ErrorNotification error={error} />;
|
return <ErrorNotification error={error} />;
|
||||||
@@ -63,7 +66,13 @@ class Changesets extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!changesets || changesets.length === 0) {
|
if (!changesets || changesets.length === 0) {
|
||||||
return null;
|
return (
|
||||||
|
<div className="panel-block">
|
||||||
|
<Notification type="info">
|
||||||
|
{t("changesets.noChangesets")}
|
||||||
|
</Notification>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -115,6 +124,7 @@ const mapStateToProps = (state: any, ownProps: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default compose(
|
export default compose(
|
||||||
|
translate("repos"),
|
||||||
withRouter,
|
withRouter,
|
||||||
connect(
|
connect(
|
||||||
mapStateToProps,
|
mapStateToProps,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
getBranches,
|
getBranches,
|
||||||
getFetchBranchesFailure,
|
getFetchBranchesFailure,
|
||||||
isFetchBranchesPending
|
isFetchBranchesPending
|
||||||
} from "../modules/branches";
|
} from "../branches/modules/branches";
|
||||||
import { compose } from "redux";
|
import { compose } from "redux";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -40,7 +40,7 @@ type Props = {
|
|||||||
t: string => string
|
t: string => string
|
||||||
};
|
};
|
||||||
|
|
||||||
class BranchRoot extends React.Component<Props> {
|
class ChangesetsRoot extends React.Component<Props> {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.fetchBranches(this.props.repository);
|
this.props.fetchBranches(this.props.repository);
|
||||||
}
|
}
|
||||||
@@ -146,4 +146,4 @@ export default compose(
|
|||||||
mapStateToProps,
|
mapStateToProps,
|
||||||
mapDispatchToProps
|
mapDispatchToProps
|
||||||
)
|
)
|
||||||
)(BranchRoot);
|
)(ChangesetsRoot);
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ class DeleteRepo extends React.Component<Props> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<hr />
|
||||||
<Subtitle subtitle={t("deleteRepo.subtitle")} />
|
<Subtitle subtitle={t("deleteRepo.subtitle")} />
|
||||||
<ErrorNotification error={error} />
|
<ErrorNotification error={error} />
|
||||||
<div className="columns">
|
<div className="columns">
|
||||||
|
|||||||
@@ -70,7 +70,6 @@ class EditRepo extends React.Component<Props> {
|
|||||||
this.props.modifyRepo(repo, this.repoModified);
|
this.props.modifyRepo(repo, this.repoModified);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<hr />
|
|
||||||
<ExtensionPoint
|
<ExtensionPoint
|
||||||
name="repo-config.route"
|
name="repo-config.route"
|
||||||
props={extensionProps}
|
props={extensionProps}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
PageActions,
|
PageActions,
|
||||||
Button,
|
Button,
|
||||||
CreateButton,
|
CreateButton,
|
||||||
|
Notification,
|
||||||
LinkPaginator,
|
LinkPaginator,
|
||||||
getPageFromMatch
|
getPageFromMatch
|
||||||
} from "@scm-manager/ui-components";
|
} from "@scm-manager/ui-components";
|
||||||
@@ -97,15 +98,16 @@ class Overview extends React.Component<Props, State> {
|
|||||||
history.push("/repos/?q=" + filter);
|
history.push("/repos/?q=" + filter);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{this.renderList()}
|
{this.renderOverview()}
|
||||||
{this.renderPageActionCreateButton()}
|
{this.renderPageActionCreateButton()}
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderList() {
|
renderRepositoryList() {
|
||||||
const { collection, page } = this.props;
|
const { collection, page, t } = this.props;
|
||||||
if (collection) {
|
|
||||||
|
if (collection._embedded && collection._embedded.repositories.length > 0) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<RepositoryList repositories={collection._embedded.repositories} />
|
<RepositoryList repositories={collection._embedded.repositories} />
|
||||||
@@ -114,6 +116,20 @@ class Overview extends React.Component<Props, State> {
|
|||||||
page={page}
|
page={page}
|
||||||
filter={this.getQueryString()}
|
filter={this.getQueryString()}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Notification type="info">{t("overview.noRepositories")}</Notification>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderOverview() {
|
||||||
|
const { collection } = this.props;
|
||||||
|
if (collection) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{this.renderRepositoryList()}
|
||||||
{this.renderCreateButton()}
|
{this.renderCreateButton()}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import { Redirect, Route, Switch } from "react-router-dom";
|
|||||||
import type { Repository } from "@scm-manager/ui-types";
|
import type { Repository } from "@scm-manager/ui-types";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CollapsibleErrorPage,
|
|
||||||
Loading,
|
Loading,
|
||||||
Navigation,
|
Navigation,
|
||||||
SubNavigation,
|
SubNavigation,
|
||||||
@@ -24,12 +23,14 @@ import {
|
|||||||
import { translate } from "react-i18next";
|
import { translate } from "react-i18next";
|
||||||
import RepositoryDetails from "../components/RepositoryDetails";
|
import RepositoryDetails from "../components/RepositoryDetails";
|
||||||
import EditRepo from "./EditRepo";
|
import EditRepo from "./EditRepo";
|
||||||
|
import BranchesOverview from "../branches/containers/BranchesOverview";
|
||||||
|
import CreateBranch from "../branches/containers/CreateBranch";
|
||||||
import Permissions from "../permissions/containers/Permissions";
|
import Permissions from "../permissions/containers/Permissions";
|
||||||
|
|
||||||
import type { History } from "history";
|
import type { History } from "history";
|
||||||
import EditRepoNavLink from "../components/EditRepoNavLink";
|
import EditRepoNavLink from "../components/EditRepoNavLink";
|
||||||
|
import BranchRoot from "../branches/containers/BranchRoot";
|
||||||
import BranchRoot from "./ChangesetsRoot";
|
import ChangesetsRoot from "./ChangesetsRoot";
|
||||||
import ChangesetView from "./ChangesetView";
|
import ChangesetView from "./ChangesetView";
|
||||||
import PermissionsNavLink from "../components/PermissionsNavLink";
|
import PermissionsNavLink from "../components/PermissionsNavLink";
|
||||||
import Sources from "../sources/containers/Sources";
|
import Sources from "../sources/containers/Sources";
|
||||||
@@ -73,9 +74,15 @@ class RepositoryRoot extends React.Component<Props> {
|
|||||||
return this.stripEndingSlash(this.props.match.url);
|
return this.stripEndingSlash(this.props.match.url);
|
||||||
};
|
};
|
||||||
|
|
||||||
matches = (route: any) => {
|
matchesBranches = (route: any) => {
|
||||||
const url = this.matchedUrl();
|
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);
|
return route.location.pathname.match(regex);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -160,23 +167,46 @@ class RepositoryRoot extends React.Component<Props> {
|
|||||||
<Route
|
<Route
|
||||||
path={`${url}/changesets`}
|
path={`${url}/changesets`}
|
||||||
render={() => (
|
render={() => (
|
||||||
<BranchRoot
|
<ChangesetsRoot
|
||||||
repository={repository}
|
repository={repository}
|
||||||
baseUrlWithBranch={`${url}/branches`}
|
baseUrlWithBranch={`${url}/branch`}
|
||||||
baseUrlWithoutBranch={`${url}/changesets`}
|
baseUrlWithoutBranch={`${url}/changesets`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path={`${url}/branches/:branch/changesets`}
|
path={`${url}/branch/:branch/changesets`}
|
||||||
render={() => (
|
render={() => (
|
||||||
<BranchRoot
|
<ChangesetsRoot
|
||||||
repository={repository}
|
repository={repository}
|
||||||
baseUrlWithBranch={`${url}/branches`}
|
baseUrlWithBranch={`${url}/branch`}
|
||||||
baseUrlWithoutBranch={`${url}/changesets`}
|
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
|
<ExtensionPoint
|
||||||
name="repository.route"
|
name="repository.route"
|
||||||
props={extensionProps}
|
props={extensionProps}
|
||||||
@@ -197,13 +227,22 @@ class RepositoryRoot extends React.Component<Props> {
|
|||||||
icon="fas fa-info-circle"
|
icon="fas fa-info-circle"
|
||||||
label={t("repositoryRoot.menu.informationNavLink")}
|
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
|
<RepositoryNavLink
|
||||||
repository={repository}
|
repository={repository}
|
||||||
linkName="changesets"
|
linkName="changesets"
|
||||||
to={`${url}/changesets/`}
|
to={`${url}/changesets/`}
|
||||||
icon="fas fa-code-branch"
|
icon="fas fa-exchange-alt"
|
||||||
label={t("repositoryRoot.menu.historyNavLink")}
|
label={t("repositoryRoot.menu.historyNavLink")}
|
||||||
activeWhenMatch={this.matches}
|
activeWhenMatch={this.matchesChangesets}
|
||||||
activeOnlyWhenExact={false}
|
activeOnlyWhenExact={false}
|
||||||
/>
|
/>
|
||||||
<RepositoryNavLink
|
<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 injectSheet from "react-jss";
|
||||||
import FileTreeLeaf from "./FileTreeLeaf";
|
import FileTreeLeaf from "./FileTreeLeaf";
|
||||||
import type { Repository, File } from "@scm-manager/ui-types";
|
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 {
|
import {
|
||||||
getFetchSourcesFailure,
|
getFetchSourcesFailure,
|
||||||
isFetchSourcesPending,
|
isFetchSourcesPending,
|
||||||
@@ -48,16 +52,34 @@ export function findParent(path: string) {
|
|||||||
|
|
||||||
class FileTree extends React.Component<Props> {
|
class FileTree extends React.Component<Props> {
|
||||||
render() {
|
render() {
|
||||||
const {
|
const { error, loading, tree } = this.props;
|
||||||
error,
|
|
||||||
loading,
|
if (error) {
|
||||||
tree,
|
return <ErrorNotification error={error} />;
|
||||||
revision,
|
}
|
||||||
path,
|
|
||||||
baseUrl,
|
if (loading) {
|
||||||
classes,
|
return <Loading />;
|
||||||
t
|
}
|
||||||
} = this.props;
|
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 {
|
const compareFiles = function(f1: File, f2: File): number {
|
||||||
if (f1.directory) {
|
if (f1.directory) {
|
||||||
@@ -75,31 +97,11 @@ 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) {
|
if (tree._embedded && tree._embedded.children) {
|
||||||
files.push(...tree._embedded.children.sort(compareFiles));
|
files.push(...tree._embedded.children.sort(compareFiles));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (files && files.length > 0) {
|
||||||
let baseUrlWithRevision = baseUrl;
|
let baseUrlWithRevision = baseUrl;
|
||||||
if (revision) {
|
if (revision) {
|
||||||
baseUrlWithRevision += "/" + encodeURIComponent(revision);
|
baseUrlWithRevision += "/" + encodeURIComponent(revision);
|
||||||
@@ -108,7 +110,6 @@ class FileTree extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="panel-block">
|
|
||||||
<table className="table table-hover table-sm is-fullwidth">
|
<table className="table table-hover table-sm is-fullwidth">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -135,9 +136,10 @@ class FileTree extends React.Component<Props> {
|
|||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
return <Notification type="info">{t("sources.noSources")}</Notification>;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = (state: any, ownProps: Props) => {
|
const mapStateToProps = (state: any, ownProps: Props) => {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
getBranches,
|
getBranches,
|
||||||
getFetchBranchesFailure,
|
getFetchBranchesFailure,
|
||||||
isFetchBranchesPending
|
isFetchBranchesPending
|
||||||
} from "../../modules/branches";
|
} from "../../branches/modules/branches";
|
||||||
import { compose } from "redux";
|
import { compose } from "redux";
|
||||||
import Content from "./Content";
|
import Content from "./Content";
|
||||||
import { fetchSources, isDirectory } from "../modules/sources";
|
import { fetchSources, isDirectory } from "../modules/sources";
|
||||||
@@ -94,9 +94,7 @@ class Sources extends React.Component<Props> {
|
|||||||
if (currentFileIsDirectory) {
|
if (currentFileIsDirectory) {
|
||||||
return (
|
return (
|
||||||
<div className="panel">
|
<div className="panel">
|
||||||
<div className="panel-heading">
|
<div className="panel-heading">{this.renderBranchSelector()}</div>
|
||||||
{this.renderBranchSelector()}
|
|
||||||
</div>
|
|
||||||
<FileTree
|
<FileTree
|
||||||
repository={repository}
|
repository={repository}
|
||||||
revision={revision}
|
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 className="is-hidden-mobile">{this.renderLink(to, user.name)}</td>
|
||||||
<td>{this.renderLink(to, user.displayName)}</td>
|
<td>{this.renderLink(to, user.displayName)}</td>
|
||||||
<td>
|
<td>
|
||||||
<a href={`mailto: ${user.mail}`}>{user.mail}</a>
|
<a href={`mailto:${user.mail}`}>{user.mail}</a>
|
||||||
</td>
|
</td>
|
||||||
<td className="is-hidden-mobile">
|
<td className="is-hidden-mobile">
|
||||||
<input type="checkbox" id="active" checked={user.active} readOnly />
|
<input type="checkbox" id="active" checked={user.active} readOnly />
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ type Props = {
|
|||||||
users: User[]
|
users: User[]
|
||||||
};
|
};
|
||||||
|
|
||||||
;
|
|
||||||
|
|
||||||
class UserTable extends React.Component<Props> {
|
class UserTable extends React.Component<Props> {
|
||||||
render() {
|
render() {
|
||||||
const { users, t } = this.props;
|
const { users, t } = this.props;
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
PageActions,
|
PageActions,
|
||||||
Button,
|
Button,
|
||||||
CreateButton,
|
CreateButton,
|
||||||
|
Notification,
|
||||||
LinkPaginator,
|
LinkPaginator,
|
||||||
getPageFromMatch
|
getPageFromMatch
|
||||||
} from "@scm-manager/ui-components";
|
} from "@scm-manager/ui-components";
|
||||||
@@ -88,14 +89,26 @@ class Users extends React.Component<Props, State> {
|
|||||||
history.push("/users/?q=" + filter);
|
history.push("/users/?q=" + filter);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<UserTable users={users} />
|
{this.renderUserTable()}
|
||||||
{this.renderPaginator()}
|
|
||||||
{this.renderCreateButton()}
|
{this.renderCreateButton()}
|
||||||
{this.renderPageActionCreateButton()}
|
{this.renderPageActionCreateButton()}
|
||||||
</Page>
|
</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 = () => {
|
renderPaginator = () => {
|
||||||
const { list, page } = this.props;
|
const { list, page } = this.props;
|
||||||
if (list) {
|
if (list) {
|
||||||
|
|||||||
@@ -698,9 +698,9 @@
|
|||||||
version "0.0.2"
|
version "0.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85"
|
resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85"
|
||||||
|
|
||||||
"@scm-manager/ui-bundler@^0.0.26":
|
"@scm-manager/ui-bundler@^0.0.27":
|
||||||
version "0.0.26"
|
version "0.0.27"
|
||||||
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.26.tgz#4676a7079b781b33fa1989c6643205c3559b1f66"
|
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.27.tgz#3ed2c7826780b9a1a9ea90464332640cfb5d54b5"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/core" "^7.0.0"
|
"@babel/core" "^7.0.0"
|
||||||
"@babel/plugin-proposal-class-properties" "^7.0.0"
|
"@babel/plugin-proposal-class-properties" "^7.0.0"
|
||||||
@@ -5669,6 +5669,10 @@ memoize-one@^4.0.0:
|
|||||||
version "4.0.3"
|
version "4.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-4.0.3.tgz#cdfdd942853f1a1b4c71c5336b8c49da0bf0273c"
|
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:
|
memoizee@0.4.X:
|
||||||
version "0.4.14"
|
version "0.4.14"
|
||||||
resolved "https://registry.yarnpkg.com/memoizee/-/memoizee-0.4.14.tgz#07a00f204699f9a95c2d9e77218271c7cd610d57"
|
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.Context;
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
@Path(AuthenticationResource.PATH)
|
@Path(AuthenticationResource.PATH)
|
||||||
@AllowAnonymousAccess
|
@AllowAnonymousAccess
|
||||||
@@ -28,6 +30,9 @@ public class AuthenticationResource {
|
|||||||
private final AccessTokenBuilderFactory tokenBuilderFactory;
|
private final AccessTokenBuilderFactory tokenBuilderFactory;
|
||||||
private final AccessTokenCookieIssuer cookieIssuer;
|
private final AccessTokenCookieIssuer cookieIssuer;
|
||||||
|
|
||||||
|
@Inject(optional = true)
|
||||||
|
private LogoutRedirection logoutRedirection;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public AuthenticationResource(AccessTokenBuilderFactory tokenBuilderFactory, AccessTokenCookieIssuer cookieIssuer)
|
public AuthenticationResource(AccessTokenBuilderFactory tokenBuilderFactory, AccessTokenCookieIssuer cookieIssuer)
|
||||||
{
|
{
|
||||||
@@ -35,7 +40,6 @@ public class AuthenticationResource {
|
|||||||
this.cookieIssuer = cookieIssuer;
|
this.cookieIssuer = cookieIssuer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
@Path("access_token")
|
@Path("access_token")
|
||||||
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||||
@@ -121,6 +125,7 @@ public class AuthenticationResource {
|
|||||||
|
|
||||||
@DELETE
|
@DELETE
|
||||||
@Path("access_token")
|
@Path("access_token")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
@StatusCodes({
|
@StatusCodes({
|
||||||
@ResponseCode(code = 204, condition = "success"),
|
@ResponseCode(code = 204, condition = "success"),
|
||||||
@ResponseCode(code = 500, condition = "internal server error")
|
@ResponseCode(code = 500, condition = "internal server error")
|
||||||
@@ -135,7 +140,19 @@ public class AuthenticationResource {
|
|||||||
cookieIssuer.invalidate(request, response);
|
cookieIssuer.invalidate(request, response);
|
||||||
|
|
||||||
// TODO anonymous access ??
|
// TODO anonymous access ??
|
||||||
|
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();
|
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")
|
@ResponseCode(code = 500, condition = "internal server error")
|
||||||
})
|
})
|
||||||
public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("branch") String branchName) throws IOException {
|
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();
|
Branches branches = repositoryService.getBranchesCommand().getBranches();
|
||||||
return branches.getBranches()
|
return branches.getBranches()
|
||||||
.stream()
|
.stream()
|
||||||
.filter(branch -> branchName.equals(branch.getName()))
|
.filter(branch -> branchName.equals(branch.getName()))
|
||||||
.findFirst()
|
.findFirst()
|
||||||
.map(branch -> branchToDtoMapper.map(branch, new NamespaceAndName(namespace, name)))
|
.map(branch -> branchToDtoMapper.map(branch, namespaceAndName))
|
||||||
.map(Response::ok)
|
.map(Response::ok)
|
||||||
.orElse(Response.status(Response.Status.NOT_FOUND))
|
.orElseThrow(() -> notFound(entity("branch", branchName).in(namespaceAndName)))
|
||||||
.build();
|
.build();
|
||||||
} catch (CommandNotSupportedException ex) {
|
} catch (CommandNotSupportedException ex) {
|
||||||
return Response.status(Response.Status.BAD_REQUEST).build();
|
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.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.net.URI;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
import java.util.Date;
|
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.assertEquals;
|
||||||
|
import static org.junit.Assert.assertThat;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
@@ -49,6 +57,8 @@ public class AuthenticationResourceTest {
|
|||||||
|
|
||||||
private AccessTokenCookieIssuer cookieIssuer = new DefaultAccessTokenCookieIssuer(mock(ScmConfiguration.class));
|
private AccessTokenCookieIssuer cookieIssuer = new DefaultAccessTokenCookieIssuer(mock(ScmConfiguration.class));
|
||||||
|
|
||||||
|
private MockHttpResponse response = new MockHttpResponse();
|
||||||
|
|
||||||
private static final String AUTH_JSON_TRILLIAN = "{\n" +
|
private static final String AUTH_JSON_TRILLIAN = "{\n" +
|
||||||
"\t\"cookie\": true,\n" +
|
"\t\"cookie\": true,\n" +
|
||||||
"\t\"grant_type\": \"password\",\n" +
|
"\t\"grant_type\": \"password\",\n" +
|
||||||
@@ -101,9 +111,11 @@ public class AuthenticationResourceTest {
|
|||||||
"}"
|
"}"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
private AuthenticationResource authenticationResource;
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void prepareEnvironment() {
|
public void prepareEnvironment() {
|
||||||
AuthenticationResource authenticationResource = new AuthenticationResource(accessTokenBuilderFactory, cookieIssuer);
|
authenticationResource = new AuthenticationResource(accessTokenBuilderFactory, cookieIssuer);
|
||||||
dispatcher.getRegistry().addSingletonResource(authenticationResource);
|
dispatcher.getRegistry().addSingletonResource(authenticationResource);
|
||||||
|
|
||||||
AccessToken accessToken = mock(AccessToken.class);
|
AccessToken accessToken = mock(AccessToken.class);
|
||||||
@@ -123,7 +135,6 @@ public class AuthenticationResourceTest {
|
|||||||
public void shouldAuthCorrectly() throws URISyntaxException {
|
public void shouldAuthCorrectly() throws URISyntaxException {
|
||||||
|
|
||||||
MockHttpRequest request = getMockHttpRequest(AUTH_JSON_TRILLIAN);
|
MockHttpRequest request = getMockHttpRequest(AUTH_JSON_TRILLIAN);
|
||||||
MockHttpResponse response = new MockHttpResponse();
|
|
||||||
|
|
||||||
dispatcher.invoke(request, response);
|
dispatcher.invoke(request, response);
|
||||||
|
|
||||||
@@ -134,7 +145,6 @@ public class AuthenticationResourceTest {
|
|||||||
public void shouldAuthCorrectlyWithFormencodedData() throws URISyntaxException {
|
public void shouldAuthCorrectlyWithFormencodedData() throws URISyntaxException {
|
||||||
|
|
||||||
MockHttpRequest request = getMockHttpRequestUrlEncoded(AUTH_FORMENCODED_TRILLIAN);
|
MockHttpRequest request = getMockHttpRequestUrlEncoded(AUTH_FORMENCODED_TRILLIAN);
|
||||||
MockHttpResponse response = new MockHttpResponse();
|
|
||||||
|
|
||||||
dispatcher.invoke(request, response);
|
dispatcher.invoke(request, response);
|
||||||
|
|
||||||
@@ -146,7 +156,6 @@ public class AuthenticationResourceTest {
|
|||||||
public void shouldNotAuthUserWithWrongPassword() throws URISyntaxException {
|
public void shouldNotAuthUserWithWrongPassword() throws URISyntaxException {
|
||||||
|
|
||||||
MockHttpRequest request = getMockHttpRequest(AUTH_JSON_TRILLIAN_WRONG_PW);
|
MockHttpRequest request = getMockHttpRequest(AUTH_JSON_TRILLIAN_WRONG_PW);
|
||||||
MockHttpResponse response = new MockHttpResponse();
|
|
||||||
|
|
||||||
dispatcher.invoke(request, response);
|
dispatcher.invoke(request, response);
|
||||||
|
|
||||||
@@ -156,7 +165,6 @@ public class AuthenticationResourceTest {
|
|||||||
@Test
|
@Test
|
||||||
public void shouldNotAuthNonexistingUser() throws URISyntaxException {
|
public void shouldNotAuthNonexistingUser() throws URISyntaxException {
|
||||||
MockHttpRequest request = getMockHttpRequest(AUTH_JSON_NOT_EXISTING_USER);
|
MockHttpRequest request = getMockHttpRequest(AUTH_JSON_NOT_EXISTING_USER);
|
||||||
MockHttpResponse response = new MockHttpResponse();
|
|
||||||
|
|
||||||
dispatcher.invoke(request, response);
|
dispatcher.invoke(request, response);
|
||||||
|
|
||||||
@@ -187,16 +195,36 @@ public class AuthenticationResourceTest {
|
|||||||
@SubjectAware(username = "trillian", password = "secret")
|
@SubjectAware(username = "trillian", password = "secret")
|
||||||
public void shouldSuccessfullyLogoutUser() throws URISyntaxException {
|
public void shouldSuccessfullyLogoutUser() throws URISyntaxException {
|
||||||
MockHttpRequest request = MockHttpRequest.delete("/" + AuthenticationResource.PATH + "/access_token");
|
MockHttpRequest request = MockHttpRequest.delete("/" + AuthenticationResource.PATH + "/access_token");
|
||||||
MockHttpResponse response = new MockHttpResponse();
|
|
||||||
|
|
||||||
dispatcher.invoke(request, response);
|
dispatcher.invoke(request, response);
|
||||||
assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus());
|
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 {
|
private void shouldReturnBadRequest(String requestBody) throws URISyntaxException {
|
||||||
MockHttpRequest request = getMockHttpRequest(requestBody);
|
MockHttpRequest request = getMockHttpRequest(requestBody);
|
||||||
MockHttpResponse response = new MockHttpResponse();
|
|
||||||
|
|
||||||
dispatcher.invoke(request, response);
|
dispatcher.invoke(request, response);
|
||||||
|
|
||||||
@@ -218,5 +246,4 @@ public class AuthenticationResourceTest {
|
|||||||
request.contentType(MediaType.APPLICATION_FORM_URLENCODED_TYPE);
|
request.contentType(MediaType.APPLICATION_FORM_URLENCODED_TYPE);
|
||||||
return request;
|
return request;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,6 +129,7 @@ public class BranchRootResourceTest extends RepositoryTestBase {
|
|||||||
dispatcher.invoke(request, response);
|
dispatcher.invoke(request, response);
|
||||||
|
|
||||||
assertEquals(404, response.getStatus());
|
assertEquals(404, response.getStatus());
|
||||||
|
assertEquals("application/vnd.scmm-error+json;v=2", response.getOutputHeaders().getFirst("Content-Type"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
Reference in New Issue
Block a user