Merged in feature/branch-overview (pull request #222)

Feature/branch overview
This commit is contained in:
Sebastian Sdorra
2019-04-03 15:54:45 +00:00
41 changed files with 1093 additions and 144 deletions

View File

@@ -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);

View 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 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);

View File

@@ -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);

View File

@@ -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,

View File

@@ -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": {

View File

@@ -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",

View 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);

View File

@@ -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
);

View File

@@ -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",

View File

@@ -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",

View File

@@ -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);

View File

@@ -4,5 +4,6 @@ import type {Links} from "./hal";
export type Branch = {
name: string,
revision: string,
defaultBranch?: boolean,
_links: Links
}

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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) {

View File

@@ -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}`

View File

@@ -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 =

View File

@@ -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

View 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);

View 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);

View 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);

View 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;

View 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);

View 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;

View 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));

View 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)
);

View 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);

View 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);

View File

@@ -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}`;

View File

@@ -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", () => {

View 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;
});
}

View 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
]);
});
});

View File

@@ -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)}

View File

@@ -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);

View File

@@ -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

View File

@@ -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";

View File

@@ -9,8 +9,6 @@ type Props = {
users: User[]
};
;
class UserTable extends React.Component<Props> {
render() {
const { users, t } = this.props;

View File

@@ -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"

View File

@@ -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();

View File

@@ -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