Merged in feature/create-branch (pull request #233)

Feature create branch
This commit is contained in:
Sebastian Sdorra
2019-04-17 11:29:46 +00:00
14 changed files with 785 additions and 172 deletions

View File

@@ -30,9 +30,9 @@
"@scm-manager/ui-types": "2.0.0-SNAPSHOT", "@scm-manager/ui-types": "2.0.0-SNAPSHOT",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"moment": "^2.22.2", "moment": "^2.22.2",
"react": "^16.5.2", "react": "^16.8.6",
"react-dom": "^16.8.6",
"react-diff-view": "^1.8.1", "react-diff-view": "^1.8.1",
"react-dom": "^16.5.2",
"react-i18next": "^7.11.0", "react-i18next": "^7.11.0",
"react-jss": "^8.6.1", "react-jss": "^8.6.1",
"react-router-dom": "^4.3.1", "react-router-dom": "^4.3.1",
@@ -63,4 +63,4 @@
] ]
] ]
} }
} }

View File

@@ -15,7 +15,8 @@ type Props = {
value?: string, value?: string,
onChange: (value: string, name?: string) => void, onChange: (value: string, name?: string) => void,
loading?: boolean, loading?: boolean,
helpText?: string helpText?: string,
disabled?: boolean
}; };
class Select extends React.Component<Props> { class Select extends React.Component<Props> {
@@ -34,7 +35,7 @@ class Select extends React.Component<Props> {
}; };
render() { render() {
const { options, value, label, helpText, loading } = this.props; const { options, value, label, helpText, loading, disabled } = this.props;
const loadingClass = loading ? "is-loading" : ""; const loadingClass = loading ? "is-loading" : "";
@@ -51,6 +52,7 @@ class Select extends React.Component<Props> {
}} }}
value={value} value={value}
onChange={this.handleInput} onChange={this.handleInput}
disabled={disabled}
> >
{options.map(opt => { {options.map(opt => {
return ( return (

View File

@@ -6623,14 +6623,14 @@ react-dom@^16.4.2:
prop-types "^15.6.2" prop-types "^15.6.2"
scheduler "^0.10.0" scheduler "^0.10.0"
react-dom@^16.5.2: react-dom@^16.8.6:
version "16.5.2" version "16.8.6"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.5.2.tgz#b69ee47aa20bab5327b2b9d7c1fe2a30f2cfa9d7" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.6.tgz#71d6303f631e8b0097f56165ef608f051ff6e10f"
dependencies: dependencies:
loose-envify "^1.1.0" loose-envify "^1.1.0"
object-assign "^4.1.1" object-assign "^4.1.1"
prop-types "^15.6.2" prop-types "^15.6.2"
schedule "^0.5.0" scheduler "^0.13.6"
react-i18next@^7.11.0: react-i18next@^7.11.0:
version "7.13.0" version "7.13.0"
@@ -6755,14 +6755,14 @@ react@^16.4.2:
prop-types "^15.6.2" prop-types "^15.6.2"
scheduler "^0.10.0" scheduler "^0.10.0"
react@^16.5.2: react@^16.8.6:
version "16.5.2" version "16.8.6"
resolved "https://registry.yarnpkg.com/react/-/react-16.5.2.tgz#19f6b444ed139baa45609eee6dc3d318b3895d42" resolved "https://registry.yarnpkg.com/react/-/react-16.8.6.tgz#ad6c3a9614fd3a4e9ef51117f54d888da01f2bbe"
dependencies: dependencies:
loose-envify "^1.1.0" loose-envify "^1.1.0"
object-assign "^4.1.1" object-assign "^4.1.1"
prop-types "^15.6.2" prop-types "^15.6.2"
schedule "^0.5.0" scheduler "^0.13.6"
read-only-stream@^2.0.0: read-only-stream@^2.0.0:
version "2.0.0" version "2.0.0"
@@ -7229,6 +7229,13 @@ scheduler@^0.10.0:
loose-envify "^1.1.0" loose-envify "^1.1.0"
object-assign "^4.1.1" object-assign "^4.1.1"
scheduler@^0.13.6:
version "0.13.6"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.13.6.tgz#466a4ec332467b31a91b9bf74e5347072e4cd889"
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
select@^1.1.2: select@^1.1.2:
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d" resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d"

View File

@@ -7,3 +7,8 @@ export type Branch = {
defaultBranch?: boolean, defaultBranch?: boolean,
_links: Links _links: Links
} }
export type BranchRequest = {
name: string,
parent: string
}

View File

@@ -9,7 +9,7 @@ export type { Group, Member } from "./Group";
export type { Repository, RepositoryCollection, RepositoryGroup } from "./Repositories"; export type { Repository, RepositoryCollection, RepositoryGroup } from "./Repositories";
export type { RepositoryType, RepositoryTypeCollection } from "./RepositoryTypes"; export type { RepositoryType, RepositoryTypeCollection } from "./RepositoryTypes";
export type { Branch } from "./Branches"; export type { Branch, BranchRequest } from "./Branches";
export type { Changeset } from "./Changesets"; export type { Changeset } from "./Changesets";

View File

@@ -17,12 +17,14 @@
"i18next-browser-languagedetector": "^2.2.2", "i18next-browser-languagedetector": "^2.2.2",
"i18next-fetch-backend": "^0.1.0", "i18next-fetch-backend": "^0.1.0",
"jss-nested": "^6.0.1", "jss-nested": "^6.0.1",
"memoize-one": "^5.0.4",
"moment": "^2.22.2", "moment": "^2.22.2",
"node-sass": "^4.9.3", "node-sass": "^4.9.3",
"postcss-easy-import": "^3.0.0", "postcss-easy-import": "^3.0.0",
"react": "^16.4.2", "query-string": "5",
"react": "^16.8.6",
"react-diff-view": "^1.8.1", "react-diff-view": "^1.8.1",
"react-dom": "^16.4.2", "react-dom": "^16.8.6",
"react-i18next": "^7.9.0", "react-i18next": "^7.9.0",
"react-jss": "^8.6.0", "react-jss": "^8.6.0",
"react-redux": "^5.0.7", "react-redux": "^5.0.7",

View File

@@ -11,7 +11,10 @@
"validation": { "validation": {
"namespace-invalid": "The repository namespace is invalid", "namespace-invalid": "The repository namespace is invalid",
"name-invalid": "The repository name is invalid", "name-invalid": "The repository name is invalid",
"contact-invalid": "Contact must be a valid mail address" "contact-invalid": "Contact must be a valid mail address",
"branch": {
"nameInvalid": "The branch name is invalid"
}
}, },
"help": { "help": {
"namespaceHelpText": "The namespace of the repository. This name will be part of the repository url.", "namespaceHelpText": "The namespace of the repository. This name will be part of the repository url.",

View File

@@ -1,16 +1,125 @@
//@flow // @flow
import React from "react"; import React from "react";
import { translate } from "react-i18next";
import type { Repository, Branch, BranchRequest } from "@scm-manager/ui-types";
import {
Select,
InputField,
SubmitButton,
validation as validator
} from "@scm-manager/ui-components";
import { orderBranches } from "../util/orderBranches";
type Props = {}; type Props = {
submitForm: BranchRequest => void,
repository: Repository,
branches: Branch[],
loading?: boolean,
transmittedName?: string,
disabled?: boolean,
t: string => string
};
type State = {
source?: string,
name?: string,
nameValidationError: boolean
};
class BranchForm extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
nameValidationError: false,
name: props.transmittedName
};
}
isFalsy(value) {
return !value;
}
isValid = () => {
const { source, name } = this.state;
return !(
this.state.nameValidationError ||
this.isFalsy(source) ||
this.isFalsy(name)
);
};
submit = (event: Event) => {
event.preventDefault();
if (this.isValid()) {
this.props.submitForm({
name: this.state.name,
parent: this.state.source
});
}
};
class CreateBranch extends React.Component<Props> {
render() { render() {
const { t, branches, loading, transmittedName, disabled } = this.props;
const { name } = this.state;
orderBranches(branches);
const options = branches.map(branch => ({
label: branch.name,
value: branch.name
}));
return ( return (
<> <>
<p>Form placeholder</p> <form onSubmit={this.submit}>
<div className="columns">
<div className="column">
<Select
name="source"
label={t("branches.create.source")}
options={options}
onChange={this.handleSourceChange}
loading={loading}
disabled={disabled}
/>
<InputField
name="name"
label={t("branches.create.name")}
onChange={this.handleNameChange}
value={name ? name : ""}
validationError={this.state.nameValidationError}
errorMessage={t("validation.branch.nameInvalid")}
disabled={!!transmittedName || disabled}
/>
</div>
</div>
<div className="columns">
<div className="column">
<SubmitButton
disabled={disabled || !this.isValid()}
loading={loading}
label={t("branches.create.submit")}
/>
</div>
</div>
</form>
</> </>
); );
} }
handleSourceChange = (source: string) => {
this.setState({
...this.state,
source
});
};
handleNameChange = (name: string) => {
this.setState({
nameValidationError: !validator.isNameValid(name),
...this.state,
name
});
};
} }
export default translate("repos")(BranchForm); export default translate("repos")(BranchForm);

View File

@@ -13,6 +13,7 @@ import {
import { ErrorNotification, Loading } from "@scm-manager/ui-components"; import { ErrorNotification, Loading } from "@scm-manager/ui-components";
import type { History } from "history"; import type { History } from "history";
import { NotFoundError } from "@scm-manager/ui-components"; import { NotFoundError } from "@scm-manager/ui-components";
import queryString from "query-string";
type Props = { type Props = {
repository: Repository, repository: Repository,
@@ -49,25 +50,25 @@ class BranchRoot extends React.Component<Props> {
}; };
render() { render() {
const { const { repository, branch, loading, error, match, location } = this.props;
repository,
branch,
loading,
error,
match,
location
} = this.props;
const url = this.matchedUrl(); const url = this.matchedUrl();
if (error) { if (error) {
if(error instanceof NotFoundError && location.search.indexOf("?create=true") > -1) { if (
return <Redirect to={`/repo/${repository.namespace}/${repository.name}/branches/create?name=${match.params.branch}`} />; error instanceof NotFoundError &&
queryString.parse(location.search).create === "true"
) {
return (
<Redirect
to={`/repo/${repository.namespace}/${
repository.name
}/branches/create?name=${match.params.branch}`}
/>
);
} }
return ( return <ErrorNotification error={error} />;
<ErrorNotification error={error} />
);
} }
if (loading || !branch) { if (loading || !branch) {

View File

@@ -4,7 +4,8 @@ import {
fetchBranches, fetchBranches,
getBranches, getBranches,
getFetchBranchesFailure, getFetchBranchesFailure,
isFetchBranchesPending isFetchBranchesPending,
isPermittedToCreateBranches
} from "../modules/branches"; } from "../modules/branches";
import { orderBranches } from "../util/orderBranches"; import { orderBranches } from "../util/orderBranches";
import { connect } from "react-redux"; import { connect } from "react-redux";
@@ -75,8 +76,7 @@ class BranchesOverview extends React.Component<Props> {
renderCreateButton() { renderCreateButton() {
const { showCreateButton, t } = this.props; const { showCreateButton, t } = this.props;
if (showCreateButton || true) { if (showCreateButton) {
// TODO
return ( return (
<CreateButton <CreateButton
label={t("branches.overview.createButton")} label={t("branches.overview.createButton")}
@@ -93,12 +93,14 @@ const mapStateToProps = (state, ownProps) => {
const loading = isFetchBranchesPending(state, repository); const loading = isFetchBranchesPending(state, repository);
const error = getFetchBranchesFailure(state, repository); const error = getFetchBranchesFailure(state, repository);
const branches = getBranches(state, repository); const branches = getBranches(state, repository);
const showCreateButton = isPermittedToCreateBranches(state, repository);
return { return {
repository, repository,
loading, loading,
error, error,
branches branches,
showCreateButton
}; };
}; };

View File

@@ -1,23 +1,150 @@
//@flow //@flow
import React from "react"; import React from "react";
import { Subtitle } from "@scm-manager/ui-components"; import {
import {translate} from "react-i18next"; ErrorNotification,
Loading,
Subtitle
} from "@scm-manager/ui-components";
import { translate } from "react-i18next";
import BranchForm from "../components/BranchForm";
import type { Repository, Branch, BranchRequest } from "@scm-manager/ui-types";
import {
fetchBranches,
getBranches,
getBranchCreateLink,
createBranch,
createBranchReset,
isCreateBranchPending,
getCreateBranchFailure,
isFetchBranchesPending,
getFetchBranchesFailure
} from "../modules/branches";
import type { History } from "history";
import { connect } from "react-redux";
import { withRouter } from "react-router-dom";
import queryString from "query-string";
type Props = { type Props = {
t: string => string loading?: boolean,
error?: Error,
repository: Repository,
branches: Branch[],
createBranchesLink: string,
isPermittedToCreateBranches: boolean,
// dispatcher functions
fetchBranches: Repository => void,
createBranch: (
createLink: string,
repository: Repository,
branch: BranchRequest,
callback?: (Branch) => void
) => void,
resetForm: Repository => void,
// context objects
t: string => string,
history: History,
location: any
}; };
class CreateBranch extends React.Component<Props> { class CreateBranch extends React.Component<Props> {
componentDidMount() {
const { fetchBranches, repository } = this.props;
fetchBranches(repository);
this.props.resetForm(repository);
}
branchCreated = (branch: Branch) => {
const { history, repository } = this.props;
history.push(
`/repo/${repository.namespace}/${
repository.name
}/branch/${encodeURIComponent(branch.name)}/info`
);
};
createBranch = (branch: BranchRequest) => {
this.props.createBranch(
this.props.createBranchesLink,
this.props.repository,
branch,
newBranch => this.branchCreated(newBranch)
);
};
transmittedName = (url: string) => {
const params = queryString.parse(url);
return params.name;
};
render() { render() {
const { t } = this.props; const { t, loading, error, repository, branches, createBranchesLink, location } = this.props;
if (error) {
return <ErrorNotification error={error} />;
}
if (loading || !branches) {
return <Loading />;
}
return ( return (
<> <>
<Subtitle subtitle={t("branches.create.title")} /> <Subtitle subtitle={t("branches.create.title")} />
<p>Create placeholder</p> <BranchForm
submitForm={branchRequest => this.createBranch(branchRequest)}
loading={loading}
repository={repository}
branches={branches}
transmittedName={this.transmittedName(location.search)}
disabled={!createBranchesLink}
/>
</> </>
); );
} }
} }
export default translate("repos")(CreateBranch); const mapDispatchToProps = dispatch => {
return {
fetchBranches: (repository: Repository) => {
dispatch(fetchBranches(repository));
},
createBranch: (
createLink: string,
repository: Repository,
branchRequest: BranchRequest,
callback?: (newBranch: Branch) => void
) => {
dispatch(createBranch(createLink, repository, branchRequest, callback));
},
resetForm: (repository: Repository) => {
dispatch(createBranchReset(repository));
}
};
};
const mapStateToProps = (state, ownProps) => {
const { repository } = ownProps;
const loading =
isFetchBranchesPending(state, repository) ||
isCreateBranchPending(state, repository);
const error =
getFetchBranchesFailure(state, repository) || getCreateBranchFailure(state);
const branches = getBranches(state, repository);
const createBranchesLink = getBranchCreateLink(state, repository);
return {
repository,
loading,
error,
branches,
createBranchesLink
};
};
export default withRouter(
connect(
mapStateToProps,
mapDispatchToProps
)(translate("repos")(CreateBranch))
);

View File

@@ -2,13 +2,21 @@
import { import {
FAILURE_SUFFIX, FAILURE_SUFFIX,
PENDING_SUFFIX, PENDING_SUFFIX,
SUCCESS_SUFFIX SUCCESS_SUFFIX,
RESET_SUFFIX
} from "../../../modules/types"; } from "../../../modules/types";
import { apiClient } from "@scm-manager/ui-components"; import { apiClient } from "@scm-manager/ui-components";
import type { Action, Branch, Repository } from "@scm-manager/ui-types"; import type {
Action,
Branch,
BranchRequest,
Repository
} from "@scm-manager/ui-types";
import { isPending } from "../../../modules/pending"; import { isPending } from "../../../modules/pending";
import { getFailure } from "../../../modules/failure"; import { getFailure } from "../../../modules/failure";
import memoizeOne from 'memoize-one';
export const FETCH_BRANCHES = "scm/repos/FETCH_BRANCHES"; export const FETCH_BRANCHES = "scm/repos/FETCH_BRANCHES";
export const FETCH_BRANCHES_PENDING = `${FETCH_BRANCHES}_${PENDING_SUFFIX}`; export const FETCH_BRANCHES_PENDING = `${FETCH_BRANCHES}_${PENDING_SUFFIX}`;
export const FETCH_BRANCHES_SUCCESS = `${FETCH_BRANCHES}_${SUCCESS_SUFFIX}`; export const FETCH_BRANCHES_SUCCESS = `${FETCH_BRANCHES}_${SUCCESS_SUFFIX}`;
@@ -19,6 +27,15 @@ export const FETCH_BRANCH_PENDING = `${FETCH_BRANCH}_${PENDING_SUFFIX}`;
export const FETCH_BRANCH_SUCCESS = `${FETCH_BRANCH}_${SUCCESS_SUFFIX}`; export const FETCH_BRANCH_SUCCESS = `${FETCH_BRANCH}_${SUCCESS_SUFFIX}`;
export const FETCH_BRANCH_FAILURE = `${FETCH_BRANCH}_${FAILURE_SUFFIX}`; export const FETCH_BRANCH_FAILURE = `${FETCH_BRANCH}_${FAILURE_SUFFIX}`;
export const CREATE_BRANCH = "scm/repos/CREATE_BRANCH";
export const CREATE_BRANCH_PENDING = `${CREATE_BRANCH}_${PENDING_SUFFIX}`;
export const CREATE_BRANCH_SUCCESS = `${CREATE_BRANCH}_${SUCCESS_SUFFIX}`;
export const CREATE_BRANCH_FAILURE = `${CREATE_BRANCH}_${FAILURE_SUFFIX}`;
export const CREATE_BRANCH_RESET = `${CREATE_BRANCH}_${RESET_SUFFIX}`;
const CONTENT_TYPE_BRANCH_REQUEST =
"application/vnd.scmm-branchRequest+json;v=2";
// Fetching branches // Fetching branches
export function fetchBranches(repository: Repository) { export function fetchBranches(repository: Repository) {
@@ -44,10 +61,7 @@ export function fetchBranches(repository: Repository) {
}; };
} }
export function fetchBranch( export function fetchBranch(repository: Repository, name: string) {
repository: Repository,
name: string
) {
let link = repository._links.branches.href; let link = repository._links.branches.href;
if (!link.endsWith("/")) { if (!link.endsWith("/")) {
link += "/"; link += "/";
@@ -69,26 +83,85 @@ export function fetchBranch(
}; };
} }
// create branch
export function createBranch(
link: string,
repository: Repository,
branchRequest: BranchRequest,
callback?: (branch: Branch) => void
) {
return function(dispatch: any) {
dispatch(createBranchPending(repository));
return apiClient
.post(link, branchRequest, CONTENT_TYPE_BRANCH_REQUEST)
.then(response => response.headers.get("Location"))
.then(location => apiClient.get(location))
.then(response => response.json())
.then(branch => {
dispatch(createBranchSuccess(repository));
if (callback) {
callback(branch);
}
})
.catch(error => dispatch(createBranchFailure(repository, error)));
};
}
// Selectors // Selectors
export function getBranches(state: Object, repository: Repository) { function collectBranches(repoState) {
const key = createKey(repository); return repoState.list._embedded.branches.map(
if (state.branches[key]) { name => repoState.byName[name]
return state.branches[key]; );
}
return null;
} }
const memoizedBranchCollector = memoizeOne(collectBranches);
export function getBranches(state: Object, repository: Repository) {
const repoState = getRepoState(state, repository);
if (repoState && repoState.list) {
return memoizedBranchCollector(repoState);
}
}
export function getBranchCreateLink(state: Object, repository: Repository) {
const repoState = getRepoState(state, repository);
if (repoState && repoState.list && repoState.list._links && repoState.list._links.create) {
return repoState.list._links.create.href;
}
}
function getRepoState(state: Object, repository: Repository) {
const key = createKey(repository);
const repoState = state.branches[key];
if (repoState && repoState.byName) {
return repoState;
}
}
export const isPermittedToCreateBranches = (
state: Object,
repository: Repository
): boolean => {
const repoState = getRepoState(state, repository);
return !!(
repoState &&
repoState.list &&
repoState.list._links &&
repoState.list._links.create
);
};
export function getBranch( export function getBranch(
state: Object, state: Object,
repository: Repository, repository: Repository,
name: string name: string
): ?Branch { ): ?Branch {
const key = createKey(repository); const repoState = getRepoState(state, repository);
if (state.branches[key]) { if (repoState) {
return state.branches[key].find((b: Branch) => b.name === name); return repoState.byName[name];
} }
return null;
} }
// Action creators // Action creators
@@ -103,14 +176,6 @@ export function getFetchBranchesFailure(state: Object, repository: Repository) {
return getFailure(state, FETCH_BRANCHES, createKey(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) { export function fetchBranchesPending(repository: Repository) {
return { return {
type: FETCH_BRANCHES_PENDING, type: FETCH_BRANCHES_PENDING,
@@ -135,6 +200,65 @@ export function fetchBranchesFailure(repository: Repository, error: Error) {
}; };
} }
export function isCreateBranchPending(state: Object, repository: Repository) {
return isPending(state, CREATE_BRANCH, createKey(repository));
}
export function getCreateBranchFailure(state: Object) {
return getFailure(state, CREATE_BRANCH);
}
export function createBranchPending(repository: Repository): Action {
return {
type: CREATE_BRANCH_PENDING,
payload: { repository },
itemId: createKey(repository)
};
}
export function createBranchSuccess(repository: Repository): Action {
return {
type: CREATE_BRANCH_SUCCESS,
payload: { repository },
itemId: createKey(repository)
};
}
export function createBranchFailure(
repository: Repository,
error: Error
): Action {
return {
type: CREATE_BRANCH_FAILURE,
payload: { repository, error },
itemId: createKey(repository)
};
}
export function createBranchReset(repository: Repository): Action {
return {
type: CREATE_BRANCH_RESET,
payload: { repository },
itemId: createKey(repository)
};
}
export function isFetchBranchPending(
state: Object,
repository: Repository,
name: string
) {
return isPending(state, FETCH_BRANCH, createKey(repository) + "/" + name);
}
export function getFetchBranchFailure(
state: Object,
repository: Repository,
name: string
) {
return getFailure(state, FETCH_BRANCH, createKey(repository) + "/" + name);
}
export function fetchBranchPending( export function fetchBranchPending(
repository: Repository, repository: Repository,
name: string name: string
@@ -171,58 +295,61 @@ export function fetchBranchFailure(
// Reducers // Reducers
function extractBranchesFromPayload(payload: any) { const reduceByBranchesSuccess = (state, payload) => {
if (payload._embedded && payload._embedded.branches) { const repository = payload.repository;
return payload._embedded.branches; const response = payload.data;
}
return [];
}
function reduceBranchSuccess(state, repositoryName, newBranch) { const key = createKey(repository);
const newBranches = []; const repoState = state[key] || {};
// we do not use filter, because we try to keep the current order const byName = repoState.byName || {};
let found = false; repoState.byName = byName;
for (const branch of state[repositoryName] || []) {
if (branch.name === newBranch.name) { const branches = response._embedded.branches;
newBranches.push(newBranch); const names = branches.map(b => b.name);
found = true; response._embedded.branches = names;
} else { for (let branch of branches) {
newBranches.push(branch); byName[branch.name] = branch;
}
return {
[key]: {
list: response,
byName
} }
} };
if (!found) { };
newBranches.push(newBranch);
}
return newBranches;
}
type State = { [string]: Branch[] }; const reduceByBranchSuccess = (state, payload) => {
const repository = payload.repository;
const branch = payload.branch;
const key = createKey(repository);
const repoState = state[key] || {};
const byName = repoState.byName || {};
byName[branch.name] = branch;
repoState.byName = byName;
return {
...state,
[key]: repoState
};
};
export default function reducer( export default function reducer(
state: State = {}, state: {} = {},
action: Action = { type: "UNKNOWN" } action: Action = { type: "UNKNOWN" }
): State { ): Object {
if (!action.payload) { if (!action.payload) {
return state; return state;
} }
const payload = action.payload; const payload = action.payload;
switch (action.type) { switch (action.type) {
case FETCH_BRANCHES_SUCCESS: case FETCH_BRANCHES_SUCCESS:
const key = createKey(payload.repository); return reduceByBranchesSuccess(state, payload);
return {
...state,
[key]: extractBranchesFromPayload(payload.data)
};
case FETCH_BRANCH_SUCCESS: case FETCH_BRANCH_SUCCESS:
if (!action.payload.repository || !action.payload.branch) { return reduceByBranchSuccess(state, payload);
return state;
}
const newBranch = action.payload.branch;
const repositoryName = createKey(action.payload.repository);
return {
...state,
[repositoryName]: reduceBranchSuccess(state, repositoryName, newBranch)
};
default: default:
return state; return state;
} }

View File

@@ -9,13 +9,21 @@ import reducer, {
FETCH_BRANCH_PENDING, FETCH_BRANCH_PENDING,
FETCH_BRANCH_SUCCESS, FETCH_BRANCH_SUCCESS,
FETCH_BRANCH_FAILURE, FETCH_BRANCH_FAILURE,
CREATE_BRANCH,
CREATE_BRANCH_FAILURE,
CREATE_BRANCH_PENDING,
CREATE_BRANCH_SUCCESS,
fetchBranches, fetchBranches,
fetchBranch, fetchBranch,
fetchBranchSuccess, fetchBranchSuccess,
getBranch, getBranch,
getBranches, getBranches,
getFetchBranchesFailure, getFetchBranchesFailure,
isFetchBranchesPending isFetchBranchesPending,
createBranch,
isCreateBranchPending,
getCreateBranchFailure,
isPermittedToCreateBranches
} from "./branches"; } from "./branches";
const namespace = "foo"; const namespace = "foo";
@@ -34,6 +42,8 @@ const repository = {
const branch1 = { name: "branch1", revision: "revision1" }; const branch1 = { name: "branch1", revision: "revision1" };
const branch2 = { name: "branch2", revision: "revision2" }; const branch2 = { name: "branch2", revision: "revision2" };
const branch3 = { name: "branch3", revision: "revision3" }; const branch3 = { name: "branch3", revision: "revision3" };
const branchRequest = { name: "newBranch", source: "master" };
const newBranch = { name: "newBranch", revision: "rev3" };
describe("branches", () => { describe("branches", () => {
describe("fetch branches", () => { describe("fetch branches", () => {
@@ -119,12 +129,84 @@ describe("branches", () => {
expect(actions[1].payload).toBeDefined(); expect(actions[1].payload).toBeDefined();
}); });
}); });
it("should create a branch successfully", () => {
//branchrequest answer
fetchMock.postOnce(URL, {
status: 201,
headers: {
location: URL + "/newBranch"
}
});
//branch answer
fetchMock.getOnce(URL + "/newBranch", newBranch);
const store = mockStore({});
return store
.dispatch(createBranch(URL, repository, branchRequest))
.then(() => {
const actions = store.getActions();
expect(actions[0].type).toEqual(CREATE_BRANCH_PENDING);
expect(actions[1].type).toEqual(CREATE_BRANCH_SUCCESS);
});
});
it("should call the callback with the branch from the location header", () => {
//branchrequest answer
fetchMock.postOnce(URL, {
status: 201,
headers: {
location: URL + "/newBranch"
}
});
//branch answer
fetchMock.getOnce(URL + "/newBranch", newBranch);
const store = mockStore({});
let receivedBranch = null;
const callback = branch => {
receivedBranch = branch;
};
return store
.dispatch(createBranch(URL, repository, branchRequest, callback))
.then(() => {
expect(receivedBranch).toEqual(newBranch);
});
});
it("should fail creating a branch on HTTP 500", () => {
fetchMock.postOnce(URL, {
status: 500
});
const store = mockStore({});
return store
.dispatch(createBranch(URL, repository, branchRequest))
.then(() => {
const actions = store.getActions();
expect(actions[0].type).toEqual(CREATE_BRANCH_PENDING);
expect(actions[1].type).toEqual(CREATE_BRANCH_FAILURE);
});
});
}); });
describe("branches reducer", () => { describe("branches reducer", () => {
const branches = { const branches = {
_embedded: { _embedded: {
branches: [branch1, branch2] branches: [branch1, branch2]
},
_links: {
self: {
href: "/self"
},
create: {
href: "/create"
}
} }
}; };
const action = { const action = {
@@ -135,81 +217,94 @@ describe("branches", () => {
} }
}; };
it("should update state according to successful fetch", () => { it("should store the branches", () => {
const newState = reducer({}, action); const newState = reducer({}, action);
expect(newState).toBeDefined(); const repoState = newState["foo/bar"];
expect(newState[key]).toBeDefined();
expect(newState[key]).toContain(branch1); expect(repoState.list._links.create.href).toEqual("/create");
expect(newState[key]).toContain(branch2); expect(repoState.list._embedded.branches).toEqual(["branch1", "branch2"]);
expect(repoState.byName.branch1).toEqual(branch1);
expect(repoState.byName.branch2).toEqual(branch2);
}); });
it("should not delete existing branches from state", () => { it("should store a single branch", () => {
const oldState = { const newState = reducer({}, fetchBranchSuccess(repository, branch1));
"hitchhiker/heartOfGold": [branch3] const repoState = newState["foo/bar"];
expect(repoState.list).toBeUndefined();
expect(repoState.byName.branch1).toEqual(branch1);
});
it("should add a single branch", () => {
const state = {
"foo/bar": {
list: {
_links: {},
_embedded: {
branches: ["branch1"]
}
},
byName: {
branch1: branch1
}
}
}; };
const newState = reducer(oldState, action); const newState = reducer(state, fetchBranchSuccess(repository, branch2));
expect(newState[key]).toContain(branch1); const repoState = newState["foo/bar"];
expect(newState[key]).toContain(branch2); const byName = repoState.byName;
expect(newState["hitchhiker/heartOfGold"]).toContain(branch3);
expect(repoState.list._embedded.branches).toEqual(["branch1"]);
expect(byName.branch1).toEqual(branch1);
expect(byName.branch2).toEqual(branch2);
}); });
it("should update state according to FETCH_BRANCH_SUCCESS action", () => { it("should not overwrite non related repositories", () => {
const newState = reducer({}, fetchBranchSuccess(repository, branch3)); const state = {
expect(newState["foo/bar"]).toEqual([branch3]); "scm/core": {
}); byName: {
branch1: branch1
it("should not delete existing branch from state", () => { }
const oldState = { }
"foo/bar": [branch1]
}; };
const newState = reducer( const newState = reducer(state, fetchBranchSuccess(repository, branch1));
oldState, const byName = newState["scm/core"].byName;
fetchBranchSuccess(repository, branch2)
); expect(byName.branch1).toEqual(branch1);
expect(newState["foo/bar"]).toEqual([branch1, branch2]);
}); });
it("should update required branch from state", () => { it("should overwrite existing branch", () => {
const oldState = { const state = {
"foo/bar": [branch1] "foo/bar": {
byName: {
branch1: {
name: "branch1",
revision: "xyz"
}
}
}
}; };
const newBranch1 = { name: "branch1", revision: "revision2" }; const newState = reducer(state, fetchBranchSuccess(repository, branch1));
const newState = reducer( const byName = newState["foo/bar"].byName;
oldState,
fetchBranchSuccess(repository, newBranch1) expect(byName.branch1.revision).toEqual("revision1");
);
expect(newState["foo/bar"]).toEqual([newBranch1]);
}); });
it("should update required branch from state and keeps old repo", () => { it("should not overwrite existing branches", () => {
const oldState = { const state = {
"ns/one": [branch1] "foo/bar": {
byName: {
branch1,
branch2,
branch3
}
}
}; };
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); const newState = reducer(state, action);
expect(newState).toBe(state); expect(newState["foo/bar"].byName.branch1).toEqual(branch1);
expect(newState["foo/bar"].byName.branch2).toEqual(branch2);
expect(newState["foo/bar"].byName.branch3).toEqual(branch3);
}); });
}); });
@@ -218,7 +313,18 @@ describe("branches", () => {
const state = { const state = {
branches: { branches: {
[key]: [branch1, branch2] "foo/bar": {
list: {
_links: {},
_embedded: {
branches: ["branch1", "branch2"]
}
},
byName: {
branch1: branch1,
branch2: branch2
}
}
} }
}; };
@@ -245,9 +351,34 @@ describe("branches", () => {
expect(one).toBe(two); expect(one).toBe(two);
}); });
it("should return null, if no branches for the repository available", () => { it("should not return cached reference, if branches have changed", () => {
const one = getBranches(state, repository);
const newState = {
branches: {
"foo/bar": {
list: {
_links: {},
_embedded: {
branches: ["branch2", "branch3"]
}
},
byName: {
branch2,
branch3
}
}
}
};
const two = getBranches(newState, repository);
expect(one).not.toBe(two);
expect(two).not.toContain(branch1);
expect(two).toContain(branch2);
expect(two).toContain(branch3);
});
it("should return undefined, if no branches for the repository available", () => {
const branches = getBranches({ branches: {} }, repository); const branches = getBranches({ branches: {} }, repository);
expect(branches).toBeNull(); expect(branches).toBeUndefined();
}); });
it("should return single branch by name", () => { it("should return single branch by name", () => {
@@ -266,6 +397,32 @@ describe("branches", () => {
expect(branch).toBeUndefined(); expect(branch).toBeUndefined();
}); });
it("should return true if the branches list contains the create link", () => {
const stateWithLink = {
branches: {
"foo/bar": {
...state.branches["foo/bar"],
list: {
...state.branches["foo/bar"].list,
_links: {
create: {
href: "http://create-it"
}
}
}
}
}
};
const permitted = isPermittedToCreateBranches(stateWithLink, repository);
expect(permitted).toBe(true);
});
it("should return false if the create link is missing", () => {
const permitted = isPermittedToCreateBranches(state, repository);
expect(permitted).toBe(false);
});
it("should return error if fetching branches failed", () => { it("should return error if fetching branches failed", () => {
const state = { const state = {
failure: { failure: {
@@ -279,5 +436,36 @@ describe("branches", () => {
it("should return false if fetching branches did not fail", () => { it("should return false if fetching branches did not fail", () => {
expect(getFetchBranchesFailure({}, repository)).toBeUndefined(); expect(getFetchBranchesFailure({}, repository)).toBeUndefined();
}); });
it("should return true if create branch is pending", () => {
const state = {
pending: {
[CREATE_BRANCH + "/foo/bar"]: true
}
};
expect(isCreateBranchPending(state, repository)).toBe(true);
});
it("should return false if create branch is not pending", () => {
const state = {
pending: {
[CREATE_BRANCH + "/foo/bar"]: false
}
};
expect(isCreateBranchPending(state, repository)).toBe(false);
});
it("should return error when create branch did fail", () => {
const state = {
failure: {
[CREATE_BRANCH]: error
}
};
expect(getCreateBranchFailure(state)).toEqual(error);
});
it("should return undefined when create branch did not fail", () => {
expect(getCreateBranchFailure({})).toBe(undefined);
});
}); });
}); });

View File

@@ -701,7 +701,6 @@
"@scm-manager/ui-bundler@^0.0.27": "@scm-manager/ui-bundler@^0.0.27":
version "0.0.27" version "0.0.27"
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.27.tgz#3ed2c7826780b9a1a9ea90464332640cfb5d54b5" resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.27.tgz#3ed2c7826780b9a1a9ea90464332640cfb5d54b5"
integrity sha512-cBU1xq6gDy1Vw9AGOzsR763+JmBeraTaC/KQfxT3I6XyZJ2brIfG1m5QYcAcHWvDxq3mYMogpI5rfShw14L4/w==
dependencies: dependencies:
"@babel/core" "^7.0.0" "@babel/core" "^7.0.0"
"@babel/plugin-proposal-class-properties" "^7.0.0" "@babel/plugin-proposal-class-properties" "^7.0.0"
@@ -5670,6 +5669,10 @@ memoize-one@^4.0.0:
version "4.0.3" version "4.0.3"
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-4.0.3.tgz#cdfdd942853f1a1b4c71c5336b8c49da0bf0273c" resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-4.0.3.tgz#cdfdd942853f1a1b4c71c5336b8c49da0bf0273c"
memoize-one@^5.0.4:
version "5.0.4"
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.0.4.tgz#005928aced5c43d890a4dfab18ca908b0ec92cbc"
memoizee@0.4.X: memoizee@0.4.X:
version "0.4.14" version "0.4.14"
resolved "https://registry.yarnpkg.com/memoizee/-/memoizee-0.4.14.tgz#07a00f204699f9a95c2d9e77218271c7cd610d57" resolved "https://registry.yarnpkg.com/memoizee/-/memoizee-0.4.14.tgz#07a00f204699f9a95c2d9e77218271c7cd610d57"
@@ -6832,6 +6835,14 @@ qs@~6.5.1, qs@~6.5.2:
version "6.5.2" version "6.5.2"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
query-string@5:
version "5.1.1"
resolved "https://registry.yarnpkg.com/query-string/-/query-string-5.1.1.tgz#a78c012b71c17e05f2e3fa2319dd330682efb3cb"
dependencies:
decode-uri-component "^0.2.0"
object-assign "^4.1.0"
strict-uri-encode "^1.0.0"
querystring-es3@~0.2.0: querystring-es3@~0.2.0:
version "0.2.1" version "0.2.1"
resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"
@@ -6921,6 +6932,15 @@ react-dom@^16.4.2:
prop-types "^15.6.2" prop-types "^15.6.2"
schedule "^0.5.0" schedule "^0.5.0"
react-dom@^16.8.6:
version "16.8.6"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.6.tgz#71d6303f631e8b0097f56165ef608f051ff6e10f"
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
prop-types "^15.6.2"
scheduler "^0.13.6"
react-i18next@^7.9.0: react-i18next@^7.9.0:
version "7.13.0" version "7.13.0"
resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-7.13.0.tgz#a6f64fd749215ec70400f90da6cbde2a9c5b1588" resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-7.13.0.tgz#a6f64fd749215ec70400f90da6cbde2a9c5b1588"
@@ -7051,6 +7071,15 @@ react@^16.4.2:
prop-types "^15.6.2" prop-types "^15.6.2"
schedule "^0.5.0" schedule "^0.5.0"
react@^16.8.6:
version "16.8.6"
resolved "https://registry.yarnpkg.com/react/-/react-16.8.6.tgz#ad6c3a9614fd3a4e9ef51117f54d888da01f2bbe"
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
prop-types "^15.6.2"
scheduler "^0.13.6"
read-cache@^1.0.0: read-cache@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774" resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774"
@@ -7563,6 +7592,13 @@ schedule@^0.5.0:
dependencies: dependencies:
object-assign "^4.1.1" object-assign "^4.1.1"
scheduler@^0.13.6:
version "0.13.6"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.13.6.tgz#466a4ec332467b31a91b9bf74e5347072e4cd889"
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
scss-tokenizer@^0.2.3: scss-tokenizer@^0.2.3:
version "0.2.3" version "0.2.3"
resolved "https://registry.yarnpkg.com/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz#8eb06db9a9723333824d3f5530641149847ce5d1" resolved "https://registry.yarnpkg.com/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz#8eb06db9a9723333824d3f5530641149847ce5d1"
@@ -7991,6 +8027,10 @@ stream-throttle@^0.1.3:
commander "^2.2.0" commander "^2.2.0"
limiter "^1.0.5" limiter "^1.0.5"
strict-uri-encode@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713"
string-length@^2.0.0: string-length@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed" resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed"