mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-02-03 21:29:18 +01:00
Merged in feature/branch-overview (pull request #222)
Feature/branch overview
This commit is contained in:
@@ -8,11 +8,10 @@ type Props = {
|
||||
repository: Repository,
|
||||
|
||||
// context props
|
||||
t: (string) => string
|
||||
t: string => string
|
||||
};
|
||||
|
||||
class CloneInformation extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
const { url, repository, t } = this.props;
|
||||
|
||||
@@ -51,7 +50,6 @@ class CloneInformation extends React.Component<Props> {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default translate("plugins")(CloneInformation);
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import type { Branch } from "@scm-manager/ui-types";
|
||||
import { translate } from "react-i18next";
|
||||
|
||||
type Props = {
|
||||
branch: Branch,
|
||||
t: string => string
|
||||
};
|
||||
|
||||
class GitBranchInformation extends React.Component<Props> {
|
||||
render() {
|
||||
const { branch, t } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h4>{t("scm-git-plugin.information.fetch")}</h4>
|
||||
<pre>
|
||||
<code>git fetch</code>
|
||||
</pre>
|
||||
<h4>{t("scm-git-plugin.information.checkout")}</h4>
|
||||
<pre>
|
||||
<code>git checkout {branch.name}</code>
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate("plugins")(GitBranchInformation);
|
||||
@@ -10,7 +10,7 @@ type Configuration = {
|
||||
gcExpression?: string,
|
||||
nonFastForwardDisallowed: boolean,
|
||||
_links: Links
|
||||
}
|
||||
};
|
||||
|
||||
type Props = {
|
||||
initialConfiguration: Configuration,
|
||||
@@ -19,25 +19,24 @@ type Props = {
|
||||
onConfigurationChange: (Configuration, boolean) => void,
|
||||
|
||||
// context props
|
||||
t: (string) => string
|
||||
}
|
||||
t: string => string
|
||||
};
|
||||
|
||||
type State = Configuration & {
|
||||
|
||||
}
|
||||
type State = Configuration & {};
|
||||
|
||||
class GitConfigurationForm extends React.Component<Props, State> {
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = { ...props.initialConfiguration };
|
||||
}
|
||||
|
||||
|
||||
handleChange = (value: any, name: string) => {
|
||||
this.setState({
|
||||
[name]: value
|
||||
}, () => this.props.onConfigurationChange(this.state, true));
|
||||
this.setState(
|
||||
{
|
||||
[name]: value
|
||||
},
|
||||
() => this.props.onConfigurationChange(this.state, true)
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
@@ -46,24 +45,25 @@ class GitConfigurationForm extends React.Component<Props, State> {
|
||||
|
||||
return (
|
||||
<>
|
||||
<InputField name="gcExpression"
|
||||
label={t("scm-git-plugin.config.gcExpression")}
|
||||
helpText={t("scm-git-plugin.config.gcExpressionHelpText")}
|
||||
value={gcExpression}
|
||||
onChange={this.handleChange}
|
||||
disabled={readOnly}
|
||||
<InputField
|
||||
name="gcExpression"
|
||||
label={t("scm-git-plugin.config.gcExpression")}
|
||||
helpText={t("scm-git-plugin.config.gcExpressionHelpText")}
|
||||
value={gcExpression}
|
||||
onChange={this.handleChange}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
<Checkbox name="nonFastForwardDisallowed"
|
||||
label={t("scm-git-plugin.config.nonFastForwardDisallowed")}
|
||||
helpText={t("scm-git-plugin.config.nonFastForwardDisallowedHelpText")}
|
||||
checked={nonFastForwardDisallowed}
|
||||
onChange={this.handleChange}
|
||||
disabled={readOnly}
|
||||
<Checkbox
|
||||
name="nonFastForwardDisallowed"
|
||||
label={t("scm-git-plugin.config.nonFastForwardDisallowed")}
|
||||
helpText={t("scm-git-plugin.config.nonFastForwardDisallowedHelpText")}
|
||||
checked={nonFastForwardDisallowed}
|
||||
onChange={this.handleChange}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default translate("plugins")(GitConfigurationForm);
|
||||
|
||||
@@ -6,6 +6,7 @@ import GitAvatar from "./GitAvatar";
|
||||
|
||||
import {ConfigurationBinder as cfgBinder} from "@scm-manager/ui-components";
|
||||
import GitGlobalConfiguration from "./GitGlobalConfiguration";
|
||||
import GitBranchInformation from "./GitBranchInformation";
|
||||
import GitMergeInformation from "./GitMergeInformation";
|
||||
import RepositoryConfig from "./RepositoryConfig";
|
||||
|
||||
@@ -20,6 +21,11 @@ binder.bind(
|
||||
ProtocolInformation,
|
||||
gitPredicate
|
||||
);
|
||||
binder.bind(
|
||||
"repos.branch-details.information",
|
||||
GitBranchInformation,
|
||||
gitPredicate
|
||||
);
|
||||
binder.bind(
|
||||
"repos.repository-merge.information",
|
||||
GitMergeInformation,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
{
|
||||
"scm-git-plugin": {
|
||||
"information": {
|
||||
"clone" : "Repository klonen",
|
||||
"create" : "Neues Repository erstellen",
|
||||
"replace" : "Ein bestehendes Repository aktualisieren",
|
||||
"clone": "Repository klonen",
|
||||
"create": "Neues Repository erstellen",
|
||||
"replace": "Ein bestehendes Repository aktualisieren",
|
||||
"fetch": "Remote-Änderungen herunterladen",
|
||||
"checkout": "Branch wechseln",
|
||||
"merge": {
|
||||
"heading": "Merge des Source Branch in den Target Branch",
|
||||
"checkout": "1. Sicherstellen, dass der Workspace aufgeräumt ist und der Target Branch ausgecheckt wurde.",
|
||||
@@ -37,7 +39,7 @@
|
||||
"success": "Der standard Branch wurde geändert!"
|
||||
}
|
||||
},
|
||||
"permissions" : {
|
||||
"permissions": {
|
||||
"configuration": {
|
||||
"read,write": {
|
||||
"git": {
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
"clone": "Clone the repository",
|
||||
"create": "Create a new repository",
|
||||
"replace": "Push an existing repository",
|
||||
"fetch": "Get remote changes",
|
||||
"checkout": "Switch branch",
|
||||
"merge": {
|
||||
"heading": "How to merge source branch into target branch",
|
||||
"checkout": "1. Make sure your workspace is clean and checkout target branch",
|
||||
|
||||
30
scm-plugins/scm-hg-plugin/src/main/js/HgBranchInformation.js
Normal file
30
scm-plugins/scm-hg-plugin/src/main/js/HgBranchInformation.js
Normal file
@@ -0,0 +1,30 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import type { Branch } from "@scm-manager/ui-types";
|
||||
import { translate } from "react-i18next";
|
||||
|
||||
type Props = {
|
||||
branch: Branch,
|
||||
t: string => string
|
||||
};
|
||||
|
||||
class HgBranchInformation extends React.Component<Props> {
|
||||
render() {
|
||||
const { branch, t } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h4>{t("scm-hg-plugin.information.fetch")}</h4>
|
||||
<pre>
|
||||
<code>hg pull</code>
|
||||
</pre>
|
||||
<h4>{t("scm-hg-plugin.information.checkout")}</h4>
|
||||
<pre>
|
||||
<code>hg update {branch.name}</code>
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate("plugins")(HgBranchInformation);
|
||||
@@ -4,14 +4,29 @@ import ProtocolInformation from "./ProtocolInformation";
|
||||
import HgAvatar from "./HgAvatar";
|
||||
import { ConfigurationBinder as cfgBinder } from "@scm-manager/ui-components";
|
||||
import HgGlobalConfiguration from "./HgGlobalConfiguration";
|
||||
import HgBranchInformation from "./HgBranchInformation";
|
||||
|
||||
const hgPredicate = (props: Object) => {
|
||||
return props.repository && props.repository.type === "hg";
|
||||
};
|
||||
|
||||
binder.bind("repos.repository-details.information", ProtocolInformation, hgPredicate);
|
||||
binder.bind(
|
||||
"repos.repository-details.information",
|
||||
ProtocolInformation,
|
||||
hgPredicate
|
||||
);
|
||||
binder.bind(
|
||||
"repos.branch-details.information",
|
||||
HgBranchInformation,
|
||||
hgPredicate
|
||||
);
|
||||
binder.bind("repos.repository-avatar", HgAvatar, hgPredicate);
|
||||
|
||||
// bind global configuration
|
||||
|
||||
cfgBinder.bindGlobal("/hg", "scm-hg-plugin.config.link", "hgConfig", HgGlobalConfiguration);
|
||||
cfgBinder.bindGlobal(
|
||||
"/hg",
|
||||
"scm-hg-plugin.config.link",
|
||||
"hgConfig",
|
||||
HgGlobalConfiguration
|
||||
);
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
"information": {
|
||||
"clone" : "Repository klonen",
|
||||
"create" : "Neues Repository erstellen",
|
||||
"replace" : "Ein bestehendes Repository aktualisieren"
|
||||
"replace" : "Ein bestehendes Repository aktualisieren",
|
||||
"fetch": "Remote-Änderungen herunterladen",
|
||||
"checkout": "Branch wechseln"
|
||||
},
|
||||
"config": {
|
||||
"link": "Mercurial",
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
"information": {
|
||||
"clone" : "Clone the repository",
|
||||
"create" : "Create a new repository",
|
||||
"replace" : "Push an existing repository"
|
||||
"replace" : "Push an existing repository",
|
||||
"fetch": "Get remote changes",
|
||||
"checkout": "Switch branch"
|
||||
},
|
||||
"config": {
|
||||
"link": "Mercurial",
|
||||
|
||||
@@ -11,11 +11,10 @@ type Props = {
|
||||
changeset: Changeset,
|
||||
|
||||
// context props
|
||||
t: (string) => string
|
||||
}
|
||||
t: string => string
|
||||
};
|
||||
|
||||
class ChangesetButtonGroup extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
const { repository, changeset, t } = this.props;
|
||||
|
||||
@@ -26,7 +25,7 @@ class ChangesetButtonGroup extends React.Component<Props> {
|
||||
<ButtonGroup className="is-pulled-right">
|
||||
<Button link={changesetLink}>
|
||||
<span className="icon">
|
||||
<i className="fas fa-code-branch"></i>
|
||||
<i className="fas fa-exchange-alt" />
|
||||
</span>
|
||||
<span className="is-hidden-mobile is-hidden-tablet-only">
|
||||
{t("changeset.buttons.details")}
|
||||
@@ -34,7 +33,7 @@ class ChangesetButtonGroup extends React.Component<Props> {
|
||||
</Button>
|
||||
<Button link={sourcesLink}>
|
||||
<span className="icon">
|
||||
<i className="fas fa-code"></i>
|
||||
<i className="fas fa-code" />
|
||||
</span>
|
||||
<span className="is-hidden-mobile is-hidden-tablet-only">
|
||||
{t("changeset.buttons.sources")}
|
||||
@@ -43,7 +42,6 @@ class ChangesetButtonGroup extends React.Component<Props> {
|
||||
</ButtonGroup>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default translate("repos")(ChangesetButtonGroup);
|
||||
|
||||
@@ -4,5 +4,6 @@ import type {Links} from "./hal";
|
||||
export type Branch = {
|
||||
name: string,
|
||||
revision: string,
|
||||
defaultBranch?: boolean,
|
||||
_links: Links
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
"pre-commit": "jest && flow && eslint src"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scm-manager/ui-bundler": "^0.0.26",
|
||||
"@scm-manager/ui-bundler": "^0.0.27",
|
||||
"concat": "^1.0.3",
|
||||
"copyfiles": "^2.0.0",
|
||||
"enzyme": "^3.3.0",
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
"menu": {
|
||||
"navigationLabel": "Repository Navigation",
|
||||
"informationNavLink": "Informationen",
|
||||
"branchesNavLink": "Branches",
|
||||
"historyNavLink": "Commits",
|
||||
"sourcesNavLink": "Sources",
|
||||
"settingsNavLink": "Einstellungen",
|
||||
@@ -42,6 +43,27 @@
|
||||
"title": "Repository erstellen",
|
||||
"subtitle": "Erstellen eines neuen Repository"
|
||||
},
|
||||
"branches": {
|
||||
"overview": {
|
||||
"title": "Übersicht aller verfügbaren Branches",
|
||||
"createButton": "Branch erstellen"
|
||||
},
|
||||
"table": {
|
||||
"branches": "Branches"
|
||||
},
|
||||
"create": {
|
||||
"title": "Branch erstellen",
|
||||
"source": "Quellbranch",
|
||||
"name": "Name",
|
||||
"submit": "Branch erstellen"
|
||||
}
|
||||
},
|
||||
"branch": {
|
||||
"name": "Name:",
|
||||
"commits": "Commits",
|
||||
"sources": "Sources",
|
||||
"defaultTag": "Default"
|
||||
},
|
||||
"changesets": {
|
||||
"errorTitle": "Fehler",
|
||||
"errorSubtitle": "Changesets konnten nicht abgerufen werden",
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
"menu": {
|
||||
"navigationLabel": "Repository Navigation",
|
||||
"informationNavLink": "Information",
|
||||
"branchesNavLink": "Branches",
|
||||
"historyNavLink": "Commits",
|
||||
"sourcesNavLink": "Sources",
|
||||
"settingsNavLink": "Settings",
|
||||
@@ -42,6 +43,27 @@
|
||||
"title": "Create Repository",
|
||||
"subtitle": "Create a new repository"
|
||||
},
|
||||
"branches": {
|
||||
"overview": {
|
||||
"title": "Overview of all branches",
|
||||
"createButton": "Create Branch"
|
||||
},
|
||||
"table": {
|
||||
"branches": "Branches"
|
||||
},
|
||||
"create": {
|
||||
"title": "Create Branch",
|
||||
"source": "Source Branch",
|
||||
"name": "Name",
|
||||
"submit": "Create Branch"
|
||||
}
|
||||
},
|
||||
"branch": {
|
||||
"name": "Name:",
|
||||
"commits": "Commits",
|
||||
"sources": "Sources",
|
||||
"defaultTag": "Default"
|
||||
},
|
||||
"changesets": {
|
||||
"errorTitle": "Error",
|
||||
"errorSubtitle": "Could not fetch changesets",
|
||||
|
||||
@@ -52,7 +52,7 @@ class Index extends Component<Props, State> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { indexResources, loading, error, t } = this.props;
|
||||
const { indexResources, loading, error } = this.props;
|
||||
const { pluginsLoaded } = this.state;
|
||||
|
||||
if (error) {
|
||||
|
||||
@@ -61,13 +61,11 @@ class PluginLoader extends React.Component<Props, State> {
|
||||
}
|
||||
return promises.reduce((chain, current) => {
|
||||
return chain.then(chainResults => {
|
||||
return current.then(currentResult => [...chainResults, currentResult])
|
||||
}
|
||||
);
|
||||
return current.then(currentResult => [...chainResults, currentResult]);
|
||||
});
|
||||
}, Promise.resolve([]));
|
||||
};
|
||||
|
||||
|
||||
loadPlugin = (plugin: Plugin) => {
|
||||
this.setState({
|
||||
message: `loading ${plugin.name}`
|
||||
|
||||
@@ -19,7 +19,7 @@ import namespaceStrategies from "./config/modules/namespaceStrategies";
|
||||
import indexResources from "./modules/indexResource";
|
||||
|
||||
import type { BrowserHistory } from "history/createBrowserHistory";
|
||||
import branches from "./repos/modules/branches";
|
||||
import branches from "./repos/branches/modules/branches";
|
||||
|
||||
function createReduxStore(history: BrowserHistory) {
|
||||
const composeEnhancers =
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import type { Group } from "@scm-manager/ui-types";
|
||||
import { Checkbox } from "@scm-manager/ui-components"
|
||||
import { Checkbox } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = {
|
||||
group: Group
|
||||
|
||||
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);
|
||||
16
scm-ui/src/repos/branches/components/BranchForm.js
Normal file
16
scm-ui/src/repos/branches/components/BranchForm.js
Normal file
@@ -0,0 +1,16 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
|
||||
type Props = {};
|
||||
|
||||
class CreateBranch extends React.Component<Props> {
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<p>Form placeholder</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
119
scm-ui/src/repos/branches/containers/BranchRoot.js
Normal file
119
scm-ui/src/repos/branches/containers/BranchRoot.js
Normal file
@@ -0,0 +1,119 @@
|
||||
//@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";
|
||||
|
||||
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 && location.search.indexOf("?create=true") > -1) {
|
||||
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)
|
||||
);
|
||||
112
scm-ui/src/repos/branches/containers/BranchesOverview.js
Normal file
112
scm-ui/src/repos/branches/containers/BranchesOverview.js
Normal file
@@ -0,0 +1,112 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import {
|
||||
fetchBranches,
|
||||
getBranches,
|
||||
getFetchBranchesFailure,
|
||||
isFetchBranchesPending
|
||||
} 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,
|
||||
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 { baseUrl, loading, error, branches, t } = this.props;
|
||||
|
||||
if (error) {
|
||||
return <ErrorNotification error={error} />;
|
||||
}
|
||||
|
||||
if (!branches || loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
orderBranches(branches);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Subtitle subtitle={t("branches.overview.title")} />
|
||||
<BranchTable baseUrl={baseUrl} branches={branches} />
|
||||
{this.renderCreateButton()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
renderCreateButton() {
|
||||
const { showCreateButton, t } = this.props;
|
||||
if (showCreateButton || true) {
|
||||
// TODO
|
||||
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);
|
||||
|
||||
return {
|
||||
repository,
|
||||
loading,
|
||||
error,
|
||||
branches
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
fetchBranches: (repository: Repository) => {
|
||||
dispatch(fetchBranches(repository));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default compose(
|
||||
translate("repos"),
|
||||
withRouter,
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)
|
||||
)(BranchesOverview);
|
||||
23
scm-ui/src/repos/branches/containers/CreateBranch.js
Normal file
23
scm-ui/src/repos/branches/containers/CreateBranch.js
Normal file
@@ -0,0 +1,23 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import { Subtitle } from "@scm-manager/ui-components";
|
||||
import {translate} from "react-i18next";
|
||||
|
||||
type Props = {
|
||||
t: string => string
|
||||
};
|
||||
|
||||
class CreateBranch extends React.Component<Props> {
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Subtitle subtitle={t("branches.create.title")} />
|
||||
<p>Create placeholder</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate("repos")(CreateBranch);
|
||||
@@ -3,17 +3,22 @@ import {
|
||||
FAILURE_SUFFIX,
|
||||
PENDING_SUFFIX,
|
||||
SUCCESS_SUFFIX
|
||||
} from "../../modules/types";
|
||||
} 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";
|
||||
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}`;
|
||||
|
||||
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}`;
|
||||
|
||||
// Fetching branches
|
||||
|
||||
export function fetchBranches(repository: Repository) {
|
||||
@@ -39,60 +44,29 @@ export function fetchBranches(repository: Repository) {
|
||||
};
|
||||
}
|
||||
|
||||
// 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;
|
||||
export function fetchBranch(
|
||||
repository: Repository,
|
||||
name: string
|
||||
) {
|
||||
let link = repository._links.branches.href;
|
||||
if (!link.endsWith("/")) {
|
||||
link += "/";
|
||||
}
|
||||
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 [];
|
||||
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));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// Selectors
|
||||
@@ -117,6 +91,7 @@ export function getBranch(
|
||||
return null;
|
||||
}
|
||||
|
||||
// Action creators
|
||||
export function isFetchBranchesPending(
|
||||
state: Object,
|
||||
repository: Repository
|
||||
@@ -128,6 +103,131 @@ export function getFetchBranchesFailure(state: Object, repository: Repository) {
|
||||
return getFailure(state, FETCH_BRANCHES, 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 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 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
|
||||
|
||||
function extractBranchesFromPayload(payload: any) {
|
||||
if (payload._embedded && payload._embedded.branches) {
|
||||
return payload._embedded.branches;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function reduceBranchSuccess(state, repositoryName, newBranch) {
|
||||
const newBranches = [];
|
||||
// we do not use filter, because we try to keep the current order
|
||||
let found = false;
|
||||
for (const branch of state[repositoryName] || []) {
|
||||
if (branch.name === newBranch.name) {
|
||||
newBranches.push(newBranch);
|
||||
found = true;
|
||||
} else {
|
||||
newBranches.push(branch);
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
newBranches.push(newBranch);
|
||||
}
|
||||
return newBranches;
|
||||
}
|
||||
|
||||
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)
|
||||
};
|
||||
case FETCH_BRANCH_SUCCESS:
|
||||
if (!action.payload.repository || !action.payload.branch) {
|
||||
return state;
|
||||
}
|
||||
const newBranch = action.payload.branch;
|
||||
const repositoryName = createKey(action.payload.repository);
|
||||
return {
|
||||
...state,
|
||||
[repositoryName]: reduceBranchSuccess(state, repositoryName, newBranch)
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
function createKey(repository: Repository): string {
|
||||
const { namespace, name } = repository;
|
||||
return `${namespace}/${name}`;
|
||||
@@ -6,7 +6,12 @@ import reducer, {
|
||||
FETCH_BRANCHES_FAILURE,
|
||||
FETCH_BRANCHES_PENDING,
|
||||
FETCH_BRANCHES_SUCCESS,
|
||||
FETCH_BRANCH_PENDING,
|
||||
FETCH_BRANCH_SUCCESS,
|
||||
FETCH_BRANCH_FAILURE,
|
||||
fetchBranches,
|
||||
fetchBranch,
|
||||
fetchBranchSuccess,
|
||||
getBranch,
|
||||
getBranches,
|
||||
getFetchBranchesFailure,
|
||||
@@ -88,6 +93,32 @@ describe("branches", () => {
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("branches reducer", () => {
|
||||
@@ -116,13 +147,70 @@ describe("branches", () => {
|
||||
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);
|
||||
});
|
||||
|
||||
it("should update state according to FETCH_BRANCH_SUCCESS action", () => {
|
||||
const newState = reducer({}, fetchBranchSuccess(repository, branch3));
|
||||
expect(newState["foo/bar"]).toEqual([branch3]);
|
||||
});
|
||||
|
||||
it("should not delete existing branch from state", () => {
|
||||
const oldState = {
|
||||
"foo/bar": [branch1]
|
||||
};
|
||||
const newState = reducer(
|
||||
oldState,
|
||||
fetchBranchSuccess(repository, branch2)
|
||||
);
|
||||
expect(newState["foo/bar"]).toEqual([branch1, branch2]);
|
||||
});
|
||||
|
||||
it("should update required branch from state", () => {
|
||||
const oldState = {
|
||||
"foo/bar": [branch1]
|
||||
};
|
||||
const newBranch1 = { name: "branch1", revision: "revision2" };
|
||||
const newState = reducer(
|
||||
oldState,
|
||||
fetchBranchSuccess(repository, newBranch1)
|
||||
);
|
||||
expect(newState["foo/bar"]).toEqual([newBranch1]);
|
||||
});
|
||||
|
||||
it("should update required branch from state and keeps old repo", () => {
|
||||
const oldState = {
|
||||
"ns/one": [branch1]
|
||||
};
|
||||
const newState = reducer(
|
||||
oldState,
|
||||
fetchBranchSuccess(repository, branch3)
|
||||
);
|
||||
expect(newState["ns/one"]).toEqual([branch1]);
|
||||
expect(newState["foo/bar"]).toEqual([branch3]);
|
||||
});
|
||||
|
||||
it("should return the oldState, if action has no payload", () => {
|
||||
const state = {};
|
||||
const newState = reducer(state, { type: FETCH_BRANCH_SUCCESS });
|
||||
expect(newState).toBe(state);
|
||||
});
|
||||
|
||||
it("should return the oldState, if payload has no branch", () => {
|
||||
const action = {
|
||||
type: FETCH_BRANCH_SUCCESS,
|
||||
payload: {
|
||||
repository
|
||||
},
|
||||
itemId: "foo/bar/"
|
||||
};
|
||||
const state = {};
|
||||
const newState = reducer(state, action);
|
||||
expect(newState).toBe(state);
|
||||
});
|
||||
});
|
||||
|
||||
describe("branch selectors", () => {
|
||||
32
scm-ui/src/repos/branches/util/orderBranches.js
Normal file
32
scm-ui/src/repos/branches/util/orderBranches.js
Normal file
@@ -0,0 +1,32 @@
|
||||
// @flow
|
||||
|
||||
// master, default should always be the first one,
|
||||
// followed by develop the rest should be ordered by its name
|
||||
import type {Branch} from "@scm-manager/ui-types";
|
||||
|
||||
export function orderBranches(branches: Branch[]) {
|
||||
branches.sort((a, b) => {
|
||||
if (a.defaultBranch && !b.defaultBranch) {
|
||||
return -20;
|
||||
} else if (!a.defaultBranch && b.defaultBranch) {
|
||||
return 20;
|
||||
} else if (a.name === "master" && b.name !== "master") {
|
||||
return -10;
|
||||
} else if (a.name !== "master" && b.name === "master") {
|
||||
return 10;
|
||||
} else if (a.name === "default" && b.name !== "default") {
|
||||
return -10;
|
||||
} else if (a.name !== "default" && b.name === "default") {
|
||||
return 10;
|
||||
} else if (a.name === "develop" && b.name !== "develop") {
|
||||
return -5;
|
||||
} else if (a.name !== "develop" && b.name === "develop") {
|
||||
return 5;
|
||||
} else if (a.name < b.name) {
|
||||
return -1;
|
||||
} else if (a.name > b.name) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
51
scm-ui/src/repos/branches/util/orderBranches.test.js
Normal file
51
scm-ui/src/repos/branches/util/orderBranches.test.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import { orderBranches } from "./orderBranches";
|
||||
|
||||
const branch1 = { name: "branch1", revision: "revision1" };
|
||||
const branch2 = { name: "branch2", revision: "revision2" };
|
||||
const branch3 = { name: "branch3", revision: "revision3", defaultBranch: true };
|
||||
const defaultBranch = {
|
||||
name: "default",
|
||||
revision: "revision4",
|
||||
defaultBranch: false
|
||||
};
|
||||
const developBranch = {
|
||||
name: "develop",
|
||||
revision: "revision5",
|
||||
defaultBranch: false
|
||||
};
|
||||
const masterBranch = {
|
||||
name: "master",
|
||||
revision: "revision6",
|
||||
defaultBranch: false
|
||||
};
|
||||
|
||||
describe("order branches", () => {
|
||||
it("should return branches", () => {
|
||||
let branches = [branch1, branch2];
|
||||
orderBranches(branches);
|
||||
expect(branches).toEqual([branch1, branch2]);
|
||||
});
|
||||
|
||||
it("should return defaultBranch first", () => {
|
||||
let branches = [branch1, branch2, branch3];
|
||||
orderBranches(branches);
|
||||
expect(branches).toEqual([branch3, branch1, branch2]);
|
||||
});
|
||||
|
||||
it("should order special branches as follows: master > default > develop", () => {
|
||||
let branches = [defaultBranch, developBranch, masterBranch];
|
||||
orderBranches(branches);
|
||||
expect(branches).toEqual([masterBranch, defaultBranch, developBranch]);
|
||||
});
|
||||
|
||||
it("should order special branches but starting with defaultBranch", () => {
|
||||
let branches = [masterBranch, developBranch, defaultBranch, branch3];
|
||||
orderBranches(branches);
|
||||
expect(branches).toEqual([
|
||||
branch3,
|
||||
masterBranch,
|
||||
defaultBranch,
|
||||
developBranch
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -35,11 +35,23 @@ class RepositoryEntry extends React.Component<Props> {
|
||||
return `/repo/${repository.namespace}/${repository.name}`;
|
||||
};
|
||||
|
||||
renderBranchesLink = (repository: Repository, repositoryLink: string) => {
|
||||
if (repository._links["branches"]) {
|
||||
return (
|
||||
<RepositoryEntryLink
|
||||
iconClass="fas fa-code-branch fa-lg"
|
||||
to={repositoryLink + "/branches"}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
renderChangesetsLink = (repository: Repository, repositoryLink: string) => {
|
||||
if (repository._links["changesets"]) {
|
||||
return (
|
||||
<RepositoryEntryLink
|
||||
iconClass="fa-code-branch fa-lg"
|
||||
iconClass="fas fa-exchange-alt fa-lg"
|
||||
to={repositoryLink + "/changesets"}
|
||||
/>
|
||||
);
|
||||
@@ -102,6 +114,7 @@ class RepositoryEntry extends React.Component<Props> {
|
||||
</div>
|
||||
<nav className="level is-mobile">
|
||||
<div className="level-left">
|
||||
{this.renderBranchesLink(repository, repositoryLink)}
|
||||
{this.renderChangesetsLink(repository, repositoryLink)}
|
||||
{this.renderSourcesLink(repository, repositoryLink)}
|
||||
{this.renderModifyLink(repository, repositoryLink)}
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
getBranches,
|
||||
getFetchBranchesFailure,
|
||||
isFetchBranchesPending
|
||||
} from "../modules/branches";
|
||||
} from "../branches/modules/branches";
|
||||
import { compose } from "redux";
|
||||
|
||||
type Props = {
|
||||
@@ -40,7 +40,7 @@ type Props = {
|
||||
t: string => string
|
||||
};
|
||||
|
||||
class BranchRoot extends React.Component<Props> {
|
||||
class ChangesetsRoot extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
this.props.fetchBranches(this.props.repository);
|
||||
}
|
||||
@@ -146,4 +146,4 @@ export default compose(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)
|
||||
)(BranchRoot);
|
||||
)(ChangesetsRoot);
|
||||
|
||||
@@ -8,33 +8,35 @@ import {
|
||||
} from "../modules/repos";
|
||||
|
||||
import { connect } from "react-redux";
|
||||
import {Redirect, Route, Switch} from "react-router-dom";
|
||||
import { Redirect, Route, Switch } from "react-router-dom";
|
||||
import type { Repository } from "@scm-manager/ui-types";
|
||||
|
||||
import {
|
||||
CollapsibleErrorPage,
|
||||
Loading,
|
||||
Navigation,
|
||||
SubNavigation,
|
||||
NavLink,
|
||||
Page,
|
||||
Section, ErrorPage
|
||||
Section,
|
||||
ErrorPage
|
||||
} from "@scm-manager/ui-components";
|
||||
import { translate } from "react-i18next";
|
||||
import RepositoryDetails from "../components/RepositoryDetails";
|
||||
import EditRepo from "./EditRepo";
|
||||
import BranchesOverview from "../branches/containers/BranchesOverview";
|
||||
import CreateBranch from "../branches/containers/CreateBranch";
|
||||
import Permissions from "../permissions/containers/Permissions";
|
||||
|
||||
import type { History } from "history";
|
||||
import EditRepoNavLink from "../components/EditRepoNavLink";
|
||||
|
||||
import BranchRoot from "./ChangesetsRoot";
|
||||
import BranchRoot from "../branches/containers/BranchRoot";
|
||||
import ChangesetsRoot from "./ChangesetsRoot";
|
||||
import ChangesetView from "./ChangesetView";
|
||||
import PermissionsNavLink from "../components/PermissionsNavLink";
|
||||
import Sources from "../sources/containers/Sources";
|
||||
import RepositoryNavLink from "../components/RepositoryNavLink";
|
||||
import {getLinks, getRepositoriesLink} from "../../modules/indexResource";
|
||||
import {binder, ExtensionPoint} from "@scm-manager/ui-extensions";
|
||||
import { getLinks, getRepositoriesLink } from "../../modules/indexResource";
|
||||
import { binder, ExtensionPoint } from "@scm-manager/ui-extensions";
|
||||
|
||||
type Props = {
|
||||
namespace: string,
|
||||
@@ -72,9 +74,15 @@ class RepositoryRoot extends React.Component<Props> {
|
||||
return this.stripEndingSlash(this.props.match.url);
|
||||
};
|
||||
|
||||
matches = (route: any) => {
|
||||
matchesBranches = (route: any) => {
|
||||
const url = this.matchedUrl();
|
||||
const regex = new RegExp(`${url}(/branches)?/?[^/]*/changesets?.*`);
|
||||
const regex = new RegExp(`${url}/branch/.+/info`);
|
||||
return route.location.pathname.match(regex);
|
||||
};
|
||||
|
||||
matchesChangesets = (route: any) => {
|
||||
const url = this.matchedUrl();
|
||||
const regex = new RegExp(`${url}(/branch)?/?[^/]*/changesets?.*`);
|
||||
return route.location.pathname.match(regex);
|
||||
};
|
||||
|
||||
@@ -82,11 +90,13 @@ class RepositoryRoot extends React.Component<Props> {
|
||||
const { loading, error, indexLinks, repository, t } = this.props;
|
||||
|
||||
if (error) {
|
||||
return <ErrorPage
|
||||
title={t("repositoryRoot.errorTitle")}
|
||||
subtitle={t("repositoryRoot.errorSubtitle")}
|
||||
error={error}
|
||||
/>
|
||||
return (
|
||||
<ErrorPage
|
||||
title={t("repositoryRoot.errorTitle")}
|
||||
subtitle={t("repositoryRoot.errorSubtitle")}
|
||||
error={error}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!repository || loading) {
|
||||
@@ -101,11 +111,14 @@ class RepositoryRoot extends React.Component<Props> {
|
||||
indexLinks
|
||||
};
|
||||
|
||||
const redirectUrlFactory = binder.getExtension("repository.redirect", this.props);
|
||||
const redirectUrlFactory = binder.getExtension(
|
||||
"repository.redirect",
|
||||
this.props
|
||||
);
|
||||
let redirectedUrl;
|
||||
if (redirectUrlFactory){
|
||||
if (redirectUrlFactory) {
|
||||
redirectedUrl = url + redirectUrlFactory(this.props);
|
||||
}else{
|
||||
} else {
|
||||
redirectedUrl = url + "/info";
|
||||
}
|
||||
|
||||
@@ -114,7 +127,7 @@ class RepositoryRoot extends React.Component<Props> {
|
||||
<div className="columns">
|
||||
<div className="column is-three-quarters is-clipped">
|
||||
<Switch>
|
||||
<Redirect exact from={this.props.match.url} to={redirectedUrl}/>
|
||||
<Redirect exact from={this.props.match.url} to={redirectedUrl} />
|
||||
<Route
|
||||
path={`${url}/info`}
|
||||
exact
|
||||
@@ -154,23 +167,46 @@ class RepositoryRoot extends React.Component<Props> {
|
||||
<Route
|
||||
path={`${url}/changesets`}
|
||||
render={() => (
|
||||
<BranchRoot
|
||||
<ChangesetsRoot
|
||||
repository={repository}
|
||||
baseUrlWithBranch={`${url}/branches`}
|
||||
baseUrlWithBranch={`${url}/branch`}
|
||||
baseUrlWithoutBranch={`${url}/changesets`}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path={`${url}/branches/:branch/changesets`}
|
||||
path={`${url}/branch/:branch`}
|
||||
render={() => (
|
||||
<BranchRoot
|
||||
repository={repository}
|
||||
baseUrlWithBranch={`${url}/branches`}
|
||||
baseUrl={`${url}/branch`}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path={`${url}/branch/:branch/changesets`}
|
||||
render={() => (
|
||||
<ChangesetsRoot
|
||||
repository={repository}
|
||||
baseUrlWithBranch={`${url}/branch`}
|
||||
baseUrlWithoutBranch={`${url}/changesets`}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path={`${url}/branches`}
|
||||
exact={true}
|
||||
render={() => (
|
||||
<BranchesOverview
|
||||
repository={repository}
|
||||
baseUrl={`${url}/branch`}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path={`${url}/branches/create`}
|
||||
render={() => <CreateBranch repository={repository} />}
|
||||
/>
|
||||
<ExtensionPoint
|
||||
name="repository.route"
|
||||
props={extensionProps}
|
||||
@@ -191,13 +227,22 @@ class RepositoryRoot extends React.Component<Props> {
|
||||
icon="fas fa-info-circle"
|
||||
label={t("repositoryRoot.menu.informationNavLink")}
|
||||
/>
|
||||
<RepositoryNavLink
|
||||
repository={repository}
|
||||
linkName="branches"
|
||||
to={`${url}/branches/`}
|
||||
icon="fas fa-code-branch"
|
||||
label={t("repositoryRoot.menu.branchesNavLink")}
|
||||
activeWhenMatch={this.matchesBranches}
|
||||
activeOnlyWhenExact={false}
|
||||
/>
|
||||
<RepositoryNavLink
|
||||
repository={repository}
|
||||
linkName="changesets"
|
||||
to={`${url}/changesets/`}
|
||||
icon="fas fa-code-branch"
|
||||
icon="fas fa-exchange-alt"
|
||||
label={t("repositoryRoot.menu.historyNavLink")}
|
||||
activeWhenMatch={this.matches}
|
||||
activeWhenMatch={this.matchesChangesets}
|
||||
activeOnlyWhenExact={false}
|
||||
/>
|
||||
<RepositoryNavLink
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
getBranches,
|
||||
getFetchBranchesFailure,
|
||||
isFetchBranchesPending
|
||||
} from "../../modules/branches";
|
||||
} from "../../branches/modules/branches";
|
||||
import { compose } from "redux";
|
||||
import Content from "./Content";
|
||||
import { fetchSources, isDirectory } from "../modules/sources";
|
||||
|
||||
@@ -9,8 +9,6 @@ type Props = {
|
||||
users: User[]
|
||||
};
|
||||
|
||||
;
|
||||
|
||||
class UserTable extends React.Component<Props> {
|
||||
render() {
|
||||
const { users, t } = this.props;
|
||||
|
||||
@@ -698,9 +698,10 @@
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85"
|
||||
|
||||
"@scm-manager/ui-bundler@^0.0.26":
|
||||
version "0.0.26"
|
||||
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.26.tgz#4676a7079b781b33fa1989c6643205c3559b1f66"
|
||||
"@scm-manager/ui-bundler@^0.0.27":
|
||||
version "0.0.27"
|
||||
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.27.tgz#3ed2c7826780b9a1a9ea90464332640cfb5d54b5"
|
||||
integrity sha512-cBU1xq6gDy1Vw9AGOzsR763+JmBeraTaC/KQfxT3I6XyZJ2brIfG1m5QYcAcHWvDxq3mYMogpI5rfShw14L4/w==
|
||||
dependencies:
|
||||
"@babel/core" "^7.0.0"
|
||||
"@babel/plugin-proposal-class-properties" "^7.0.0"
|
||||
|
||||
@@ -79,15 +79,16 @@ public class BranchRootResource {
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("branch") String branchName) throws IOException {
|
||||
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
|
||||
NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name);
|
||||
try (RepositoryService repositoryService = serviceFactory.create(namespaceAndName)) {
|
||||
Branches branches = repositoryService.getBranchesCommand().getBranches();
|
||||
return branches.getBranches()
|
||||
.stream()
|
||||
.filter(branch -> branchName.equals(branch.getName()))
|
||||
.findFirst()
|
||||
.map(branch -> branchToDtoMapper.map(branch, new NamespaceAndName(namespace, name)))
|
||||
.map(branch -> branchToDtoMapper.map(branch, namespaceAndName))
|
||||
.map(Response::ok)
|
||||
.orElse(Response.status(Response.Status.NOT_FOUND))
|
||||
.orElseThrow(() -> notFound(entity("branch", branchName).in(namespaceAndName)))
|
||||
.build();
|
||||
} catch (CommandNotSupportedException ex) {
|
||||
return Response.status(Response.Status.BAD_REQUEST).build();
|
||||
|
||||
@@ -129,6 +129,7 @@ public class BranchRootResourceTest extends RepositoryTestBase {
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertEquals(404, response.getStatus());
|
||||
assertEquals("application/vnd.scmm-error+json;v=2", response.getOutputHeaders().getFirst("Content-Type"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user