mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-08 22:45:45 +01:00
Merge with 2.0.0-m3
This commit is contained in:
@@ -398,6 +398,11 @@ public final class LogCommandBuilder
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public LogCommandBuilder setAncestorChangeset(String ancestorChangeset) {
|
||||||
|
request.setAncestorChangeset(ancestorChangeset);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
//~--- inner classes --------------------------------------------------------
|
//~--- inner classes --------------------------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -109,7 +109,10 @@ public final class DiffCommandRequest extends FileBaseCommandRequest
|
|||||||
this.format = format;
|
this.format = format;
|
||||||
}
|
}
|
||||||
|
|
||||||
//~--- get methods ----------------------------------------------------------
|
public void setAncestorChangeset(String ancestorChangeset) {
|
||||||
|
this.ancestorChangeset = ancestorChangeset;
|
||||||
|
}
|
||||||
|
//~--- get methods ----------------------------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the output format of the diff command.
|
* Return the output format of the diff command.
|
||||||
@@ -124,8 +127,13 @@ public final class DiffCommandRequest extends FileBaseCommandRequest
|
|||||||
return format;
|
return format;
|
||||||
}
|
}
|
||||||
|
|
||||||
//~--- fields ---------------------------------------------------------------
|
public String getAncestorChangeset() {
|
||||||
|
return ancestorChangeset;
|
||||||
|
}
|
||||||
|
//~--- fields ---------------------------------------------------------------
|
||||||
|
|
||||||
/** diff format */
|
/** diff format */
|
||||||
private DiffFormat format = DiffFormat.NATIVE;
|
private DiffFormat format = DiffFormat.NATIVE;
|
||||||
|
|
||||||
|
private String ancestorChangeset;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,7 +84,8 @@ public final class LogCommandRequest implements Serializable, Resetable
|
|||||||
&& Objects.equal(pagingStart, other.pagingStart)
|
&& Objects.equal(pagingStart, other.pagingStart)
|
||||||
&& Objects.equal(pagingLimit, other.pagingLimit)
|
&& Objects.equal(pagingLimit, other.pagingLimit)
|
||||||
&& Objects.equal(path, other.path)
|
&& Objects.equal(path, other.path)
|
||||||
&& Objects.equal(branch, other.branch);
|
&& Objects.equal(branch, other.branch)
|
||||||
|
&& Objects.equal(ancestorChangeset, other.ancestorChangeset);
|
||||||
//J+
|
//J+
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +99,7 @@ public final class LogCommandRequest implements Serializable, Resetable
|
|||||||
public int hashCode()
|
public int hashCode()
|
||||||
{
|
{
|
||||||
return Objects.hashCode(startChangeset, endChangeset, pagingStart,
|
return Objects.hashCode(startChangeset, endChangeset, pagingStart,
|
||||||
pagingLimit, path, branch);
|
pagingLimit, path, branch, ancestorChangeset);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -114,6 +115,7 @@ public final class LogCommandRequest implements Serializable, Resetable
|
|||||||
pagingLimit = 20;
|
pagingLimit = 20;
|
||||||
path = null;
|
path = null;
|
||||||
branch = null;
|
branch = null;
|
||||||
|
ancestorChangeset = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -133,6 +135,7 @@ public final class LogCommandRequest implements Serializable, Resetable
|
|||||||
.add("pagingLimit", pagingLimit)
|
.add("pagingLimit", pagingLimit)
|
||||||
.add("path", path)
|
.add("path", path)
|
||||||
.add("branch", branch)
|
.add("branch", branch)
|
||||||
|
.add("ancestorChangeset", ancestorChangeset)
|
||||||
.toString();
|
.toString();
|
||||||
//J+
|
//J+
|
||||||
}
|
}
|
||||||
@@ -205,6 +208,10 @@ public final class LogCommandRequest implements Serializable, Resetable
|
|||||||
this.startChangeset = startChangeset;
|
this.startChangeset = startChangeset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setAncestorChangeset(String ancestorChangeset) {
|
||||||
|
this.ancestorChangeset = ancestorChangeset;
|
||||||
|
}
|
||||||
|
|
||||||
//~--- get methods ----------------------------------------------------------
|
//~--- get methods ----------------------------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -284,6 +291,10 @@ public final class LogCommandRequest implements Serializable, Resetable
|
|||||||
return pagingLimit < 0;
|
return pagingLimit < 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getAncestorChangeset() {
|
||||||
|
return ancestorChangeset;
|
||||||
|
}
|
||||||
|
|
||||||
//~--- fields ---------------------------------------------------------------
|
//~--- fields ---------------------------------------------------------------
|
||||||
|
|
||||||
/** Field description */
|
/** Field description */
|
||||||
@@ -303,4 +314,6 @@ public final class LogCommandRequest implements Serializable, Resetable
|
|||||||
|
|
||||||
/** Field description */
|
/** Field description */
|
||||||
private String startChangeset;
|
private String startChangeset;
|
||||||
|
|
||||||
|
private String ancestorChangeset;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@scm-manager/scm-git-plugin",
|
"name": "@scm-manager/scm-git-plugin",
|
||||||
"license" : "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"main": "src/main/js/index.js",
|
"main": "src/main/js/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "ui-bundler plugin",
|
"build": "ui-bundler plugin",
|
||||||
@@ -9,9 +9,9 @@
|
|||||||
"flow": "flow check"
|
"flow": "flow check"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@scm-manager/ui-extensions": "^0.0.7"
|
"@scm-manager/ui-extensions": "^0.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@scm-manager/ui-bundler": "^0.0.19"
|
"@scm-manager/ui-bundler": "^0.0.21"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,17 +119,17 @@ public class GitRepositoryHandler
|
|||||||
public void init(SCMContextProvider context)
|
public void init(SCMContextProvider context)
|
||||||
{
|
{
|
||||||
super.init(context);
|
super.init(context);
|
||||||
scheduleGc();
|
scheduleGc(getConfig().getGcExpression());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setConfig(GitConfig config)
|
public void setConfig(GitConfig config)
|
||||||
{
|
{
|
||||||
|
scheduleGc(config.getGcExpression());
|
||||||
super.setConfig(config);
|
super.setConfig(config);
|
||||||
scheduleGc();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void scheduleGc()
|
private void scheduleGc(String expression)
|
||||||
{
|
{
|
||||||
synchronized (LOCK){
|
synchronized (LOCK){
|
||||||
if ( task != null ){
|
if ( task != null ){
|
||||||
@@ -137,11 +137,10 @@ public class GitRepositoryHandler
|
|||||||
task.cancel();
|
task.cancel();
|
||||||
task = null;
|
task = null;
|
||||||
}
|
}
|
||||||
String exp = getConfig().getGcExpression();
|
if (!Strings.isNullOrEmpty(expression))
|
||||||
if (!Strings.isNullOrEmpty(exp))
|
|
||||||
{
|
{
|
||||||
logger.info("schedule git gc task with expression {}", exp);
|
logger.info("schedule git gc task with expression {}", expression);
|
||||||
task = scheduler.schedule(exp, GitGcTask.class);
|
task = scheduler.schedule(expression, GitGcTask.class);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ import org.eclipse.jgit.lib.ObjectId;
|
|||||||
import org.eclipse.jgit.lib.Ref;
|
import org.eclipse.jgit.lib.Ref;
|
||||||
import org.eclipse.jgit.revwalk.RevCommit;
|
import org.eclipse.jgit.revwalk.RevCommit;
|
||||||
import org.eclipse.jgit.revwalk.RevWalk;
|
import org.eclipse.jgit.revwalk.RevWalk;
|
||||||
|
import org.eclipse.jgit.revwalk.filter.RevFilter;
|
||||||
import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
|
import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
|
||||||
import org.eclipse.jgit.transport.FetchResult;
|
import org.eclipse.jgit.transport.FetchResult;
|
||||||
import org.eclipse.jgit.transport.RefSpec;
|
import org.eclipse.jgit.transport.RefSpec;
|
||||||
@@ -716,6 +717,18 @@ public final class GitUtil
|
|||||||
return (id != null) &&!id.equals(ObjectId.zeroId());
|
return (id != null) &&!id.equals(ObjectId.zeroId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the first common ancestor of two revisions, aka merge base.
|
||||||
|
*/
|
||||||
|
public static ObjectId computeCommonAncestor(org.eclipse.jgit.lib.Repository repository, ObjectId revision1, ObjectId revision2) throws IOException {
|
||||||
|
try (RevWalk mergeBaseWalk = new RevWalk(repository)) {
|
||||||
|
mergeBaseWalk.setRevFilter(RevFilter.MERGE_BASE);
|
||||||
|
mergeBaseWalk.markStart(mergeBaseWalk.lookupCommit(revision1));
|
||||||
|
mergeBaseWalk.markStart(mergeBaseWalk.parseCommit(revision2));
|
||||||
|
return mergeBaseWalk.next().getId();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//~--- methods --------------------------------------------------------------
|
//~--- methods --------------------------------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -34,27 +34,25 @@ package sonia.scm.repository.spi;
|
|||||||
|
|
||||||
//~--- non-JDK imports --------------------------------------------------------
|
//~--- non-JDK imports --------------------------------------------------------
|
||||||
|
|
||||||
|
import com.google.common.base.Strings;
|
||||||
import org.eclipse.jgit.diff.DiffEntry;
|
import org.eclipse.jgit.diff.DiffEntry;
|
||||||
import org.eclipse.jgit.diff.DiffFormatter;
|
import org.eclipse.jgit.diff.DiffFormatter;
|
||||||
|
import org.eclipse.jgit.lib.ObjectId;
|
||||||
import org.eclipse.jgit.revwalk.RevCommit;
|
import org.eclipse.jgit.revwalk.RevCommit;
|
||||||
import org.eclipse.jgit.revwalk.RevTree;
|
import org.eclipse.jgit.revwalk.RevTree;
|
||||||
import org.eclipse.jgit.revwalk.RevWalk;
|
import org.eclipse.jgit.revwalk.RevWalk;
|
||||||
import org.eclipse.jgit.treewalk.EmptyTreeIterator;
|
import org.eclipse.jgit.treewalk.EmptyTreeIterator;
|
||||||
import org.eclipse.jgit.treewalk.TreeWalk;
|
import org.eclipse.jgit.treewalk.TreeWalk;
|
||||||
import org.eclipse.jgit.treewalk.filter.PathFilter;
|
import org.eclipse.jgit.treewalk.filter.PathFilter;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import sonia.scm.repository.GitUtil;
|
import sonia.scm.repository.GitUtil;
|
||||||
import sonia.scm.repository.Repository;
|
import sonia.scm.repository.Repository;
|
||||||
import sonia.scm.util.Util;
|
import sonia.scm.util.Util;
|
||||||
|
|
||||||
//~--- JDK imports ------------------------------------------------------------
|
|
||||||
|
|
||||||
import java.io.BufferedOutputStream;
|
import java.io.BufferedOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -107,7 +105,8 @@ public class GitDiffCommand extends AbstractGitCommand implements DiffCommand
|
|||||||
|
|
||||||
walk = new RevWalk(gr);
|
walk = new RevWalk(gr);
|
||||||
|
|
||||||
RevCommit commit = walk.parseCommit(gr.resolve(request.getRevision()));
|
ObjectId revision = gr.resolve(request.getRevision());
|
||||||
|
RevCommit commit = walk.parseCommit(revision);
|
||||||
|
|
||||||
walk.markStart(commit);
|
walk.markStart(commit);
|
||||||
commit = walk.next();
|
commit = walk.next();
|
||||||
@@ -120,7 +119,15 @@ public class GitDiffCommand extends AbstractGitCommand implements DiffCommand
|
|||||||
treeWalk.setFilter(PathFilter.create(request.getPath()));
|
treeWalk.setFilter(PathFilter.create(request.getPath()));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (commit.getParentCount() > 0)
|
|
||||||
|
if (!Strings.isNullOrEmpty(request.getAncestorChangeset()))
|
||||||
|
{
|
||||||
|
ObjectId otherRevision = gr.resolve(request.getAncestorChangeset());
|
||||||
|
ObjectId ancestorId = computeCommonAncestor(gr, revision, otherRevision);
|
||||||
|
RevTree tree = walk.parseCommit(ancestorId).getTree();
|
||||||
|
treeWalk.addTree(tree);
|
||||||
|
}
|
||||||
|
else if (commit.getParentCount() > 0)
|
||||||
{
|
{
|
||||||
RevTree tree = commit.getParent(0).getTree();
|
RevTree tree = commit.getParent(0).getTree();
|
||||||
|
|
||||||
@@ -156,7 +163,6 @@ public class GitDiffCommand extends AbstractGitCommand implements DiffCommand
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|
||||||
// TODO throw exception
|
// TODO throw exception
|
||||||
logger.error("could not create diff", ex);
|
logger.error("could not create diff", ex);
|
||||||
}
|
}
|
||||||
@@ -167,4 +173,9 @@ public class GitDiffCommand extends AbstractGitCommand implements DiffCommand
|
|||||||
GitUtil.release(formatter);
|
GitUtil.release(formatter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ObjectId computeCommonAncestor(org.eclipse.jgit.lib.Repository repository, ObjectId revision1, ObjectId revision2) throws IOException {
|
||||||
|
return GitUtil.computeCommonAncestor(repository, revision1, revision2);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import org.eclipse.jgit.lib.Ref;
|
|||||||
import org.eclipse.jgit.lib.Repository;
|
import org.eclipse.jgit.lib.Repository;
|
||||||
import org.eclipse.jgit.revwalk.RevCommit;
|
import org.eclipse.jgit.revwalk.RevCommit;
|
||||||
import org.eclipse.jgit.revwalk.RevWalk;
|
import org.eclipse.jgit.revwalk.RevWalk;
|
||||||
|
import org.eclipse.jgit.revwalk.filter.RevFilter;
|
||||||
import org.eclipse.jgit.treewalk.filter.AndTreeFilter;
|
import org.eclipse.jgit.treewalk.filter.AndTreeFilter;
|
||||||
import org.eclipse.jgit.treewalk.filter.PathFilter;
|
import org.eclipse.jgit.treewalk.filter.PathFilter;
|
||||||
import org.eclipse.jgit.treewalk.filter.TreeFilter;
|
import org.eclipse.jgit.treewalk.filter.TreeFilter;
|
||||||
@@ -198,6 +199,14 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
|
|||||||
endId = repository.resolve(request.getEndChangeset());
|
endId = repository.resolve(request.getEndChangeset());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ref branch = getBranchOrDefault(repository,request.getBranch());
|
||||||
|
|
||||||
|
ObjectId ancestorId = null;
|
||||||
|
|
||||||
|
if (!Strings.isNullOrEmpty(request.getAncestorChangeset())) {
|
||||||
|
ancestorId = computeCommonAncestor(request, repository, startId, branch);
|
||||||
|
}
|
||||||
|
|
||||||
revWalk = new RevWalk(repository);
|
revWalk = new RevWalk(repository);
|
||||||
|
|
||||||
converter = new GitChangesetConverter(repository, revWalk);
|
converter = new GitChangesetConverter(repository, revWalk);
|
||||||
@@ -208,8 +217,6 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
|
|||||||
PathFilter.create(request.getPath()), TreeFilter.ANY_DIFF));
|
PathFilter.create(request.getPath()), TreeFilter.ANY_DIFF));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ref branch = getBranchOrDefault(repository,request.getBranch());
|
|
||||||
|
|
||||||
if (branch != null) {
|
if (branch != null) {
|
||||||
if (startId != null) {
|
if (startId != null) {
|
||||||
revWalk.markStart(revWalk.lookupCommit(startId));
|
revWalk.markStart(revWalk.lookupCommit(startId));
|
||||||
@@ -217,11 +224,16 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
|
|||||||
revWalk.markStart(revWalk.lookupCommit(branch.getObjectId()));
|
revWalk.markStart(revWalk.lookupCommit(branch.getObjectId()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Iterator<RevCommit> iterator = revWalk.iterator();
|
Iterator<RevCommit> iterator = revWalk.iterator();
|
||||||
|
|
||||||
while (iterator.hasNext()) {
|
while (iterator.hasNext()) {
|
||||||
RevCommit commit = iterator.next();
|
RevCommit commit = iterator.next();
|
||||||
|
|
||||||
|
if (commit.getId().equals(ancestorId)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if ((counter >= start)
|
if ((counter >= start)
|
||||||
&& ((limit < 0) || (counter < start + limit))) {
|
&& ((limit < 0) || (counter < start + limit))) {
|
||||||
changesetList.add(converter.createChangeset(commit));
|
changesetList.add(converter.createChangeset(commit));
|
||||||
@@ -229,7 +241,7 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
|
|||||||
|
|
||||||
counter++;
|
counter++;
|
||||||
|
|
||||||
if ((endId != null) && commit.getId().equals(endId)) {
|
if (commit.getId().equals(endId)) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -263,4 +275,17 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
|
|||||||
|
|
||||||
return changesets;
|
return changesets;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ObjectId computeCommonAncestor(LogCommandRequest request, Repository repository, ObjectId startId, Ref branch) throws IOException {
|
||||||
|
try (RevWalk mergeBaseWalk = new RevWalk(repository)) {
|
||||||
|
mergeBaseWalk.setRevFilter(RevFilter.MERGE_BASE);
|
||||||
|
if (startId != null) {
|
||||||
|
mergeBaseWalk.markStart(mergeBaseWalk.lookupCommit(startId));
|
||||||
|
} else {
|
||||||
|
mergeBaseWalk.markStart(mergeBaseWalk.lookupCommit(branch.getObjectId()));
|
||||||
|
}
|
||||||
|
mergeBaseWalk.markStart(mergeBaseWalk.parseCommit(repository.resolve(request.getAncestorChangeset())));
|
||||||
|
return mergeBaseWalk.next().getId();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
//@flow
|
||||||
|
import React from "react";
|
||||||
|
import { translate } from "react-i18next";
|
||||||
|
import type { Links } from "@scm-manager/ui-types";
|
||||||
|
|
||||||
|
import { InputField, Checkbox } from "@scm-manager/ui-components";
|
||||||
|
|
||||||
|
type Configuration = {
|
||||||
|
repositoryDirectory?: string,
|
||||||
|
gcExpression?: string,
|
||||||
|
disabled: boolean,
|
||||||
|
_links: Links
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
initialConfiguration: Configuration,
|
||||||
|
readOnly: boolean,
|
||||||
|
|
||||||
|
onConfigurationChange: (Configuration, boolean) => void,
|
||||||
|
|
||||||
|
// context props
|
||||||
|
t: (string) => string
|
||||||
|
}
|
||||||
|
|
||||||
|
type State = Configuration & {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class GitConfigurationForm extends React.Component<Props, State> {
|
||||||
|
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
this.state = { ...props.initialConfiguration };
|
||||||
|
}
|
||||||
|
|
||||||
|
isValid = () => {
|
||||||
|
return !!this.state.repositoryDirectory;
|
||||||
|
};
|
||||||
|
|
||||||
|
handleChange = (value: any, name: string) => {
|
||||||
|
this.setState({
|
||||||
|
[name]: value
|
||||||
|
}, () => this.props.onConfigurationChange(this.state, this.isValid()));
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { repositoryDirectory, gcExpression, disabled } = this.state;
|
||||||
|
const { readOnly, t } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<InputField name="repositoryDirectory"
|
||||||
|
label={t("scm-git-plugin.config.directory")}
|
||||||
|
helpText={t("scm-git-plugin.config.directoryHelpText")}
|
||||||
|
value={repositoryDirectory}
|
||||||
|
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="disabled"
|
||||||
|
label={t("scm-git-plugin.config.disabled")}
|
||||||
|
helpText={t("scm-git-plugin.config.disabledHelpText")}
|
||||||
|
checked={disabled}
|
||||||
|
onChange={this.handleChange}
|
||||||
|
disabled={readOnly}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate("plugins")(GitConfigurationForm);
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
//@flow
|
||||||
|
import React from "react";
|
||||||
|
import { translate } from "react-i18next";
|
||||||
|
import { Title, GlobalConfiguration } from "@scm-manager/ui-components";
|
||||||
|
import GitConfigurationForm from "./GitConfigurationForm";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
link: string,
|
||||||
|
|
||||||
|
t: (string) => string
|
||||||
|
};
|
||||||
|
|
||||||
|
class GitGlobalConfiguration extends React.Component<Props> {
|
||||||
|
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { link, t } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Title title={t("scm-git-plugin.config.title")}/>
|
||||||
|
<GlobalConfiguration link={link} render={props => <GitConfigurationForm {...props} />}/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate("plugins")(GitGlobalConfiguration);
|
||||||
@@ -3,9 +3,18 @@ import { binder } from "@scm-manager/ui-extensions";
|
|||||||
import ProtocolInformation from "./ProtocolInformation";
|
import ProtocolInformation from "./ProtocolInformation";
|
||||||
import GitAvatar from "./GitAvatar";
|
import GitAvatar from "./GitAvatar";
|
||||||
|
|
||||||
|
import { ConfigurationBinder as cfgBinder } from "@scm-manager/ui-components";
|
||||||
|
import GitGlobalConfiguration from "./GitGlobalConfiguration";
|
||||||
|
|
||||||
|
// repository
|
||||||
|
|
||||||
const gitPredicate = (props: Object) => {
|
const gitPredicate = (props: Object) => {
|
||||||
return props.repository && props.repository.type === "git";
|
return props.repository && props.repository.type === "git";
|
||||||
};
|
};
|
||||||
|
|
||||||
binder.bind("repos.repository-details.information", ProtocolInformation, gitPredicate);
|
binder.bind("repos.repository-details.information", ProtocolInformation, gitPredicate);
|
||||||
binder.bind("repos.repository-avatar", GitAvatar, gitPredicate);
|
binder.bind("repos.repository-avatar", GitAvatar, gitPredicate);
|
||||||
|
|
||||||
|
// global config
|
||||||
|
|
||||||
|
cfgBinder.bindGlobal("/git", "scm-git-plugin.config.link", "gitConfig", GitGlobalConfiguration);
|
||||||
|
|||||||
@@ -4,6 +4,17 @@
|
|||||||
"clone" : "Clone the repository",
|
"clone" : "Clone the repository",
|
||||||
"create" : "Create a new repository",
|
"create" : "Create a new repository",
|
||||||
"replace" : "Push an existing repository"
|
"replace" : "Push an existing repository"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"link": "Git",
|
||||||
|
"title": "Git Configuration",
|
||||||
|
"directory": "Repository Directory",
|
||||||
|
"directoryHelpText": "Location of the Git repositories.",
|
||||||
|
"gcExpression": "GC Cron Expression",
|
||||||
|
"gcExpressionHelpText": "Use Quartz Cron Expressions (SECOND MINUTE HOUR DAYOFMONTH MONTH DAYOFWEEK) to run git gc in intervals.",
|
||||||
|
"disabled": "Disabled",
|
||||||
|
"disabledHelpText": "Enable or disable the Git plugin",
|
||||||
|
"submit": "Submit"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
package sonia.scm.repository.spi;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
public class GitDiffCommandTest extends AbstractGitCommandTestBase {
|
||||||
|
|
||||||
|
public static final String DIFF_FILE_A = "diff --git a/a.txt b/a.txt\n" +
|
||||||
|
"index 7898192..1dc60c7 100644\n" +
|
||||||
|
"--- a/a.txt\n" +
|
||||||
|
"+++ b/a.txt\n" +
|
||||||
|
"@@ -1 +1 @@\n" +
|
||||||
|
"-a\n" +
|
||||||
|
"+a and b\n";
|
||||||
|
public static final String DIFF_FILE_B = "diff --git a/b.txt b/b.txt\n" +
|
||||||
|
"deleted file mode 100644\n" +
|
||||||
|
"index 6178079..0000000\n" +
|
||||||
|
"--- a/b.txt\n" +
|
||||||
|
"+++ /dev/null\n" +
|
||||||
|
"@@ -1 +0,0 @@\n" +
|
||||||
|
"-b\n";
|
||||||
|
public static final String DIFF_FILE_A_MULTIPLE_REVISIONS = "diff --git a/a.txt b/a.txt\n" +
|
||||||
|
"index 7898192..2f8bc28 100644\n" +
|
||||||
|
"--- a/a.txt\n" +
|
||||||
|
"+++ b/a.txt\n" +
|
||||||
|
"@@ -1 +1,2 @@\n" +
|
||||||
|
" a\n" +
|
||||||
|
"+line for blame\n";
|
||||||
|
public static final String DIFF_FILE_F_MULTIPLE_REVISIONS = "diff --git a/f.txt b/f.txt\n" +
|
||||||
|
"new file mode 100644\n" +
|
||||||
|
"index 0000000..6a69f92\n" +
|
||||||
|
"--- /dev/null\n" +
|
||||||
|
"+++ b/f.txt\n" +
|
||||||
|
"@@ -0,0 +1 @@\n" +
|
||||||
|
"+f\n";
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void diffForOneRevisionShouldCreateDiff() {
|
||||||
|
GitDiffCommand gitDiffCommand = new GitDiffCommand(createContext(), repository);
|
||||||
|
DiffCommandRequest diffCommandRequest = new DiffCommandRequest();
|
||||||
|
diffCommandRequest.setRevision("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4");
|
||||||
|
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||||
|
gitDiffCommand.getDiffResult(diffCommandRequest, output);
|
||||||
|
assertEquals(DIFF_FILE_A + DIFF_FILE_B, output.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void diffForOneBranchShouldCreateDiff() {
|
||||||
|
GitDiffCommand gitDiffCommand = new GitDiffCommand(createContext(), repository);
|
||||||
|
DiffCommandRequest diffCommandRequest = new DiffCommandRequest();
|
||||||
|
diffCommandRequest.setRevision("test-branch");
|
||||||
|
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||||
|
gitDiffCommand.getDiffResult(diffCommandRequest, output);
|
||||||
|
assertEquals(DIFF_FILE_A + DIFF_FILE_B, output.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void diffForPathShouldCreateLimitedDiff() {
|
||||||
|
GitDiffCommand gitDiffCommand = new GitDiffCommand(createContext(), repository);
|
||||||
|
DiffCommandRequest diffCommandRequest = new DiffCommandRequest();
|
||||||
|
diffCommandRequest.setRevision("test-branch");
|
||||||
|
diffCommandRequest.setPath("a.txt");
|
||||||
|
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||||
|
gitDiffCommand.getDiffResult(diffCommandRequest, output);
|
||||||
|
assertEquals(DIFF_FILE_A, output.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void diffBetweenTwoBranchesShouldCreateDiff() {
|
||||||
|
GitDiffCommand gitDiffCommand = new GitDiffCommand(createContext(), repository);
|
||||||
|
DiffCommandRequest diffCommandRequest = new DiffCommandRequest();
|
||||||
|
diffCommandRequest.setRevision("master");
|
||||||
|
diffCommandRequest.setAncestorChangeset("test-branch");
|
||||||
|
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||||
|
gitDiffCommand.getDiffResult(diffCommandRequest, output);
|
||||||
|
assertEquals(DIFF_FILE_A_MULTIPLE_REVISIONS + DIFF_FILE_F_MULTIPLE_REVISIONS, output.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void diffBetweenTwoBranchesForPathShouldCreateLimitedDiff() {
|
||||||
|
GitDiffCommand gitDiffCommand = new GitDiffCommand(createContext(), repository);
|
||||||
|
DiffCommandRequest diffCommandRequest = new DiffCommandRequest();
|
||||||
|
diffCommandRequest.setRevision("master");
|
||||||
|
diffCommandRequest.setAncestorChangeset("test-branch");
|
||||||
|
diffCommandRequest.setPath("a.txt");
|
||||||
|
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||||
|
gitDiffCommand.getDiffResult(diffCommandRequest, output);
|
||||||
|
assertEquals(DIFF_FILE_A_MULTIPLE_REVISIONS, output.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -64,7 +64,7 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase
|
|||||||
* Tests log command with the usage of a default branch.
|
* Tests log command with the usage of a default branch.
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
public void testGetDefaultBranch() throws Exception {
|
public void testGetDefaultBranch() {
|
||||||
// without default branch, the repository head should be used
|
// without default branch, the repository head should be used
|
||||||
ChangesetPagingResult result = createCommand().getChangesets(new LogCommandRequest());
|
ChangesetPagingResult result = createCommand().getChangesets(new LogCommandRequest());
|
||||||
|
|
||||||
@@ -92,7 +92,7 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testGetAll() throws Exception
|
public void testGetAll()
|
||||||
{
|
{
|
||||||
ChangesetPagingResult result =
|
ChangesetPagingResult result =
|
||||||
createCommand().getChangesets(new LogCommandRequest());
|
createCommand().getChangesets(new LogCommandRequest());
|
||||||
@@ -103,7 +103,7 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testGetAllByPath() throws Exception
|
public void testGetAllByPath()
|
||||||
{
|
{
|
||||||
LogCommandRequest request = new LogCommandRequest();
|
LogCommandRequest request = new LogCommandRequest();
|
||||||
|
|
||||||
@@ -119,7 +119,7 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testGetAllWithLimit() throws Exception
|
public void testGetAllWithLimit()
|
||||||
{
|
{
|
||||||
LogCommandRequest request = new LogCommandRequest();
|
LogCommandRequest request = new LogCommandRequest();
|
||||||
|
|
||||||
@@ -143,7 +143,7 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testGetAllWithPaging() throws Exception
|
public void testGetAllWithPaging()
|
||||||
{
|
{
|
||||||
LogCommandRequest request = new LogCommandRequest();
|
LogCommandRequest request = new LogCommandRequest();
|
||||||
|
|
||||||
@@ -194,7 +194,7 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testGetRange() throws Exception
|
public void testGetRange()
|
||||||
{
|
{
|
||||||
LogCommandRequest request = new LogCommandRequest();
|
LogCommandRequest request = new LogCommandRequest();
|
||||||
|
|
||||||
@@ -216,6 +216,26 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase
|
|||||||
assertEquals("435df2f061add3589cb326cc64be9b9c3897ceca", c2.getId());
|
assertEquals("435df2f061add3589cb326cc64be9b9c3897ceca", c2.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetAncestor()
|
||||||
|
{
|
||||||
|
LogCommandRequest request = new LogCommandRequest();
|
||||||
|
|
||||||
|
request.setBranch("test-branch");
|
||||||
|
request.setAncestorChangeset("master");
|
||||||
|
|
||||||
|
ChangesetPagingResult result = createCommand().getChangesets(request);
|
||||||
|
|
||||||
|
assertNotNull(result);
|
||||||
|
assertEquals(1, result.getTotal());
|
||||||
|
assertEquals(1, result.getChangesets().size());
|
||||||
|
|
||||||
|
Changeset c = result.getChangesets().get(0);
|
||||||
|
|
||||||
|
assertNotNull(c);
|
||||||
|
assertEquals("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4", c.getId());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shouldFindDefaultBranchFromHEAD() throws Exception {
|
public void shouldFindDefaultBranchFromHEAD() throws Exception {
|
||||||
setRepositoryHeadReference("ref: refs/heads/test-branch");
|
setRepositoryHeadReference("ref: refs/heads/test-branch");
|
||||||
|
|||||||
@@ -707,9 +707,9 @@
|
|||||||
version "0.0.2"
|
version "0.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85"
|
resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85"
|
||||||
|
|
||||||
"@scm-manager/ui-bundler@^0.0.19":
|
"@scm-manager/ui-bundler@^0.0.21":
|
||||||
version "0.0.19"
|
version "0.0.21"
|
||||||
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.19.tgz#646ab1fa1e5389fad614542215c60678fb9816ae"
|
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.21.tgz#f8b5fa355415cc67b8aaf8744e1701a299dff647"
|
||||||
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"
|
||||||
@@ -747,9 +747,9 @@
|
|||||||
vinyl-source-stream "^2.0.0"
|
vinyl-source-stream "^2.0.0"
|
||||||
watchify "^3.11.0"
|
watchify "^3.11.0"
|
||||||
|
|
||||||
"@scm-manager/ui-extensions@^0.0.7":
|
"@scm-manager/ui-extensions@^0.1.1":
|
||||||
version "0.0.7"
|
version "0.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/@scm-manager/ui-extensions/-/ui-extensions-0.0.7.tgz#a0a657a1410b78838ba0b36096ef631dca7fe27e"
|
resolved "https://registry.yarnpkg.com/@scm-manager/ui-extensions/-/ui-extensions-0.1.1.tgz#966e62d89981e92a14adf7e674e646e76de96d45"
|
||||||
dependencies:
|
dependencies:
|
||||||
react "^16.4.2"
|
react "^16.4.2"
|
||||||
react-dom "^16.4.2"
|
react-dom "^16.4.2"
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
"build": "ui-bundler plugin"
|
"build": "ui-bundler plugin"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@scm-manager/ui-extensions": "^0.0.7"
|
"@scm-manager/ui-extensions": "^0.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@scm-manager/ui-bundler": "^0.0.19"
|
"@scm-manager/ui-bundler": "^0.0.21"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
113
scm-plugins/scm-hg-plugin/src/main/js/HgConfigurationForm.js
Normal file
113
scm-plugins/scm-hg-plugin/src/main/js/HgConfigurationForm.js
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
//@flow
|
||||||
|
import React from "react";
|
||||||
|
import type { Links } from "@scm-manager/ui-types";
|
||||||
|
import { translate } from "react-i18next";
|
||||||
|
import { InputField, Checkbox } from "@scm-manager/ui-components";
|
||||||
|
|
||||||
|
type Configuration = {
|
||||||
|
"hgBinary": string,
|
||||||
|
"pythonBinary": string,
|
||||||
|
"pythonPath"?: string,
|
||||||
|
"repositoryDirectory": string,
|
||||||
|
"encoding": string,
|
||||||
|
"useOptimizedBytecode": boolean,
|
||||||
|
"showRevisionInId": boolean,
|
||||||
|
"disabled": boolean,
|
||||||
|
"_links": Links
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
initialConfiguration: Configuration,
|
||||||
|
readOnly: boolean,
|
||||||
|
|
||||||
|
onConfigurationChange: (Configuration, boolean) => void,
|
||||||
|
|
||||||
|
// context props
|
||||||
|
t: (string) => string
|
||||||
|
}
|
||||||
|
|
||||||
|
type State = Configuration & {
|
||||||
|
validationErrors: string[]
|
||||||
|
};
|
||||||
|
|
||||||
|
class HgConfigurationForm extends React.Component<Props, State> {
|
||||||
|
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
this.state = { ...props.initialConfiguration, validationErrors: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
updateValidationStatus = () => {
|
||||||
|
const requiredFields = [
|
||||||
|
"hgBinary", "pythonBinary", "repositoryDirectory", "encoding"
|
||||||
|
];
|
||||||
|
|
||||||
|
const validationErrors = [];
|
||||||
|
for (let field of requiredFields) {
|
||||||
|
if (!this.state[field]) {
|
||||||
|
validationErrors.push( field );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
validationErrors
|
||||||
|
});
|
||||||
|
|
||||||
|
return validationErrors.length === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
hasValidationError = (name: string) => {
|
||||||
|
return this.state.validationErrors.indexOf(name) >= 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
handleChange = (value: any, name: string) => {
|
||||||
|
this.setState({
|
||||||
|
[name]: value
|
||||||
|
}, () => this.props.onConfigurationChange(this.state, this.updateValidationStatus()));
|
||||||
|
};
|
||||||
|
|
||||||
|
inputField = (name: string) => {
|
||||||
|
const { readOnly, t } = this.props;
|
||||||
|
return <InputField
|
||||||
|
name={ name }
|
||||||
|
label={t("scm-hg-plugin.config." + name)}
|
||||||
|
helpText={t("scm-hg-plugin.config." + name + "HelpText")}
|
||||||
|
value={this.state[name]}
|
||||||
|
onChange={this.handleChange}
|
||||||
|
validationError={this.hasValidationError(name)}
|
||||||
|
errorMessage={t("scm-hg-plugin.config.required")}
|
||||||
|
disabled={readOnly}
|
||||||
|
/>;
|
||||||
|
};
|
||||||
|
|
||||||
|
checkbox = (name: string) => {
|
||||||
|
const { readOnly, t } = this.props;
|
||||||
|
return <Checkbox
|
||||||
|
name={ name }
|
||||||
|
label={t("scm-hg-plugin.config." + name)}
|
||||||
|
helpText={t("scm-hg-plugin.config." + name + "HelpText")}
|
||||||
|
checked={this.state[name]}
|
||||||
|
onChange={this.handleChange}
|
||||||
|
disabled={readOnly}
|
||||||
|
/>;
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{this.inputField("hgBinary")}
|
||||||
|
{this.inputField("pythonBinary")}
|
||||||
|
{this.inputField("pythonPath")}
|
||||||
|
{this.inputField("repositoryDirectory")}
|
||||||
|
{this.inputField("encoding")}
|
||||||
|
{this.checkbox("useOptimizedBytecode")}
|
||||||
|
{this.checkbox("showRevisionInId")}
|
||||||
|
{this.checkbox("disabled")}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate("plugins")(HgConfigurationForm);
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
//@flow
|
||||||
|
import React from "react";
|
||||||
|
import { Title, GlobalConfiguration } from "@scm-manager/ui-components";
|
||||||
|
import { translate } from "react-i18next";
|
||||||
|
import HgConfigurationForm from "./HgConfigurationForm";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
link: string,
|
||||||
|
|
||||||
|
// context props
|
||||||
|
t: (string) => string
|
||||||
|
}
|
||||||
|
|
||||||
|
class HgGlobalConfiguration extends React.Component<Props> {
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { link, t } = this.props;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Title title={t("scm-hg-plugin.config.title")}/>
|
||||||
|
<GlobalConfiguration link={link} render={props => <HgConfigurationForm {...props} />}/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate("plugins")(HgGlobalConfiguration);
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
import { binder } from "@scm-manager/ui-extensions";
|
import { binder } from "@scm-manager/ui-extensions";
|
||||||
import ProtocolInformation from "./ProtocolInformation";
|
import ProtocolInformation from "./ProtocolInformation";
|
||||||
import HgAvatar from "./HgAvatar";
|
import HgAvatar from "./HgAvatar";
|
||||||
|
import { ConfigurationBinder as cfgBinder } from "@scm-manager/ui-components";
|
||||||
|
import HgGlobalConfiguration from "./HgGlobalConfiguration";
|
||||||
|
|
||||||
const hgPredicate = (props: Object) => {
|
const hgPredicate = (props: Object) => {
|
||||||
return props.repository && props.repository.type === "hg";
|
return props.repository && props.repository.type === "hg";
|
||||||
@@ -9,3 +11,7 @@ const hgPredicate = (props: Object) => {
|
|||||||
|
|
||||||
binder.bind("repos.repository-details.information", ProtocolInformation, hgPredicate);
|
binder.bind("repos.repository-details.information", ProtocolInformation, hgPredicate);
|
||||||
binder.bind("repos.repository-avatar", HgAvatar, hgPredicate);
|
binder.bind("repos.repository-avatar", HgAvatar, hgPredicate);
|
||||||
|
|
||||||
|
// bind global configuration
|
||||||
|
|
||||||
|
cfgBinder.bindGlobal("/hg", "scm-hg-plugin.config.link", "hgConfig", HgGlobalConfiguration);
|
||||||
|
|||||||
@@ -4,6 +4,27 @@
|
|||||||
"clone" : "Clone the repository",
|
"clone" : "Clone the repository",
|
||||||
"create" : "Create a new repository",
|
"create" : "Create a new repository",
|
||||||
"replace" : "Push an existing repository"
|
"replace" : "Push an existing repository"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"link": "Mercurial",
|
||||||
|
"title": "Mercurial Configuration",
|
||||||
|
"hgBinary": "HG Binary",
|
||||||
|
"hgBinaryHelpText": "Location of Mercurial binary.",
|
||||||
|
"pythonBinary": "Python Binary",
|
||||||
|
"pythonBinaryHelpText": "Location of Python binary.",
|
||||||
|
"pythonPath": "Python Module Search Path",
|
||||||
|
"pythonPathHelpText": "Python Module Search Path (PYTHONPATH).",
|
||||||
|
"repositoryDirectory": "Repository directory",
|
||||||
|
"repositoryDirectoryHelpText": "Location of Mercurial repositories.",
|
||||||
|
"encoding": "Encoding",
|
||||||
|
"encodingHelpText": "Repository Encoding.",
|
||||||
|
"useOptimizedBytecode": "Optimized Bytecode (.pyo)",
|
||||||
|
"useOptimizedBytecodeHelpText": "Use the Python '-O' switch.",
|
||||||
|
"showRevisionInId": "Show Revision",
|
||||||
|
"showRevisionInIdHelpText": "Show revision as part of the node id.",
|
||||||
|
"disabled": "Disabled",
|
||||||
|
"disabledHelpText": "Enable or disable the Mercurial plugin.",
|
||||||
|
"required": "This configuration value is required"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -641,9 +641,9 @@
|
|||||||
version "0.0.2"
|
version "0.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85"
|
resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85"
|
||||||
|
|
||||||
"@scm-manager/ui-bundler@^0.0.19":
|
"@scm-manager/ui-bundler@^0.0.21":
|
||||||
version "0.0.19"
|
version "0.0.21"
|
||||||
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.19.tgz#646ab1fa1e5389fad614542215c60678fb9816ae"
|
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.21.tgz#f8b5fa355415cc67b8aaf8744e1701a299dff647"
|
||||||
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"
|
||||||
@@ -660,7 +660,6 @@
|
|||||||
browserify-css "^0.14.0"
|
browserify-css "^0.14.0"
|
||||||
colors "^1.3.1"
|
colors "^1.3.1"
|
||||||
commander "^2.17.1"
|
commander "^2.17.1"
|
||||||
connect-history-api-fallback "^1.5.0"
|
|
||||||
eslint "^5.4.0"
|
eslint "^5.4.0"
|
||||||
eslint-config-react-app "^2.1.0"
|
eslint-config-react-app "^2.1.0"
|
||||||
eslint-plugin-flowtype "^2.50.0"
|
eslint-plugin-flowtype "^2.50.0"
|
||||||
@@ -682,9 +681,9 @@
|
|||||||
vinyl-source-stream "^2.0.0"
|
vinyl-source-stream "^2.0.0"
|
||||||
watchify "^3.11.0"
|
watchify "^3.11.0"
|
||||||
|
|
||||||
"@scm-manager/ui-extensions@^0.0.7":
|
"@scm-manager/ui-extensions@^0.1.1":
|
||||||
version "0.0.7"
|
version "0.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/@scm-manager/ui-extensions/-/ui-extensions-0.0.7.tgz#a0a657a1410b78838ba0b36096ef631dca7fe27e"
|
resolved "https://registry.yarnpkg.com/@scm-manager/ui-extensions/-/ui-extensions-0.1.1.tgz#966e62d89981e92a14adf7e674e646e76de96d45"
|
||||||
dependencies:
|
dependencies:
|
||||||
react "^16.4.2"
|
react "^16.4.2"
|
||||||
react-dom "^16.4.2"
|
react-dom "^16.4.2"
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
"build": "ui-bundler plugin"
|
"build": "ui-bundler plugin"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@scm-manager/ui-extensions": "^0.0.7"
|
"@scm-manager/ui-extensions": "^0.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@scm-manager/ui-bundler": "^0.0.19"
|
"@scm-manager/ui-bundler": "^0.0.21"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
107
scm-plugins/scm-svn-plugin/src/main/js/SvnConfigurationForm.js
Normal file
107
scm-plugins/scm-svn-plugin/src/main/js/SvnConfigurationForm.js
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
//@flow
|
||||||
|
import React from "react";
|
||||||
|
import type { Links } from "@scm-manager/ui-types";
|
||||||
|
import { translate } from "react-i18next";
|
||||||
|
import { InputField, Checkbox, Select } from "@scm-manager/ui-components";
|
||||||
|
|
||||||
|
type Configuration = {
|
||||||
|
repositoryDirectory: string,
|
||||||
|
compatibility: string,
|
||||||
|
enabledGZip: boolean,
|
||||||
|
disabled: boolean,
|
||||||
|
_links: Links
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
initialConfiguration: Configuration,
|
||||||
|
readOnly: boolean,
|
||||||
|
|
||||||
|
onConfigurationChange: (Configuration, boolean) => void,
|
||||||
|
|
||||||
|
// context props
|
||||||
|
t: (string) => string
|
||||||
|
}
|
||||||
|
|
||||||
|
type State = Configuration;
|
||||||
|
|
||||||
|
class HgConfigurationForm extends React.Component<Props, State> {
|
||||||
|
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
this.state = { ...props.initialConfiguration, validationErrors: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
isValid = () => {
|
||||||
|
return !!this.state.repositoryDirectory;
|
||||||
|
};
|
||||||
|
|
||||||
|
handleChange = (value: any, name: string) => {
|
||||||
|
this.setState({
|
||||||
|
[name]: value
|
||||||
|
}, () => this.props.onConfigurationChange(this.state, this.isValid()));
|
||||||
|
};
|
||||||
|
|
||||||
|
compatibilityOptions = (values: string[]) => {
|
||||||
|
const options = [];
|
||||||
|
for (let value of values) {
|
||||||
|
options.push( this.compatibilityOption(value) );
|
||||||
|
}
|
||||||
|
return options;
|
||||||
|
};
|
||||||
|
|
||||||
|
compatibilityOption = (value: string) => {
|
||||||
|
return {
|
||||||
|
value,
|
||||||
|
label: this.props.t("scm-svn-plugin.config.compatibility-values." + value.toLowerCase())
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { readOnly, t } = this.props;
|
||||||
|
const compatibilityOptions = this.compatibilityOptions([
|
||||||
|
"NONE", "PRE14", "PRE15", "PRE16", "PRE17", "WITH17"
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<InputField
|
||||||
|
name="repositoryDirectory"
|
||||||
|
label={t("scm-svn-plugin.config.directory")}
|
||||||
|
helpText={t("scm-svn-plugin.config.directoryHelpText")}
|
||||||
|
value={this.state.repositoryDirectory}
|
||||||
|
errorMessage={t("scm-svn-plugin.config.required")}
|
||||||
|
validationError={!this.state.repositoryDirectory}
|
||||||
|
onChange={this.handleChange}
|
||||||
|
disabled={readOnly}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
name="compatibility"
|
||||||
|
label={t("scm-svn-plugin.config.compatibility")}
|
||||||
|
helpText={t("scm-svn-plugin.config.compatibilityHelpText")}
|
||||||
|
value={this.state.compatibility}
|
||||||
|
options={compatibilityOptions}
|
||||||
|
onChange={this.handleChange}
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
name="enabledGZip"
|
||||||
|
label={t("scm-svn-plugin.config.enabledGZip")}
|
||||||
|
helpText={t("scm-svn-plugin.config.enabledGZipHelpText")}
|
||||||
|
checked={this.state.enabledGZip}
|
||||||
|
onChange={this.handleChange}
|
||||||
|
disabled={readOnly}
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
name="disabled"
|
||||||
|
label={t("scm-svn-plugin.config.disabled")}
|
||||||
|
helpText={t("scm-svn-plugin.config.disabledHelpText")}
|
||||||
|
checked={this.state.disabled}
|
||||||
|
onChange={this.handleChange}
|
||||||
|
disabled={readOnly}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate("plugins")(HgConfigurationForm);
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
//@flow
|
||||||
|
import React from "react";
|
||||||
|
import { translate } from "react-i18next";
|
||||||
|
import { Title, GlobalConfiguration } from "@scm-manager/ui-components";
|
||||||
|
import SvnConfigurationForm from "./SvnConfigurationForm";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
link: string,
|
||||||
|
|
||||||
|
// context props
|
||||||
|
t: (string) => string
|
||||||
|
}
|
||||||
|
|
||||||
|
class SvnGlobalConfiguration extends React.Component<Props> {
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { link, t } = this.props;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Title title={t("scm-svn-plugin.config.title")}/>
|
||||||
|
<GlobalConfiguration link={link} render={props => <SvnConfigurationForm {...props} />}/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate("plugins")(SvnGlobalConfiguration);
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import { binder } from "@scm-manager/ui-extensions";
|
import { binder } from "@scm-manager/ui-extensions";
|
||||||
|
import { ConfigurationBinder as cfgBinder } from "@scm-manager/ui-components";
|
||||||
import ProtocolInformation from "./ProtocolInformation";
|
import ProtocolInformation from "./ProtocolInformation";
|
||||||
import SvnAvatar from "./SvnAvatar";
|
import SvnAvatar from "./SvnAvatar";
|
||||||
|
import SvnGlobalConfiguration from "./SvnGlobalConfiguration";
|
||||||
|
|
||||||
const svnPredicate = (props: Object) => {
|
const svnPredicate = (props: Object) => {
|
||||||
return props.repository && props.repository.type === "svn";
|
return props.repository && props.repository.type === "svn";
|
||||||
@@ -9,3 +11,7 @@ const svnPredicate = (props: Object) => {
|
|||||||
|
|
||||||
binder.bind("repos.repository-details.information", ProtocolInformation, svnPredicate);
|
binder.bind("repos.repository-details.information", ProtocolInformation, svnPredicate);
|
||||||
binder.bind("repos.repository-avatar", SvnAvatar, svnPredicate);
|
binder.bind("repos.repository-avatar", SvnAvatar, svnPredicate);
|
||||||
|
|
||||||
|
// bind global configuration
|
||||||
|
|
||||||
|
cfgBinder.bindGlobal("/svn", "scm-svn-plugin.config.link", "svnConfig", SvnGlobalConfiguration);
|
||||||
|
|||||||
@@ -2,6 +2,27 @@
|
|||||||
"scm-svn-plugin": {
|
"scm-svn-plugin": {
|
||||||
"information": {
|
"information": {
|
||||||
"checkout" : "Checkout repository"
|
"checkout" : "Checkout repository"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"link": "Subversion",
|
||||||
|
"title": "Subversion Configuration",
|
||||||
|
"directory": "Repository Directory",
|
||||||
|
"directoryHelpText": "Location of Subversion repositories.",
|
||||||
|
"compatibility": "Version Compatibility",
|
||||||
|
"compatibilityHelpText": "Specifies with which subversion version repositories are compatible.",
|
||||||
|
"compatibility-values": {
|
||||||
|
"none": "No compatibility",
|
||||||
|
"pre14": "Pre 1.4 Compatible",
|
||||||
|
"pre15": "Pre 1.5 Compatible",
|
||||||
|
"pre16": "Pre 1.6 Compatible",
|
||||||
|
"pre17": "Pre 1.7 Compatible",
|
||||||
|
"with17": "With 1.7 Compatible"
|
||||||
|
},
|
||||||
|
"enabledGZip": "Enable GZip Compression",
|
||||||
|
"enabledGZipHelpText": "Enable GZip compression for svn responses.",
|
||||||
|
"disabled": "Disabled",
|
||||||
|
"disabledHelpText": "Enable or disable the Git plugin",
|
||||||
|
"required": "This configuration value is required"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -641,9 +641,9 @@
|
|||||||
version "0.0.2"
|
version "0.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85"
|
resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85"
|
||||||
|
|
||||||
"@scm-manager/ui-bundler@^0.0.19":
|
"@scm-manager/ui-bundler@^0.0.21":
|
||||||
version "0.0.19"
|
version "0.0.21"
|
||||||
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.19.tgz#646ab1fa1e5389fad614542215c60678fb9816ae"
|
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.21.tgz#f8b5fa355415cc67b8aaf8744e1701a299dff647"
|
||||||
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"
|
||||||
@@ -681,9 +681,9 @@
|
|||||||
vinyl-source-stream "^2.0.0"
|
vinyl-source-stream "^2.0.0"
|
||||||
watchify "^3.11.0"
|
watchify "^3.11.0"
|
||||||
|
|
||||||
"@scm-manager/ui-extensions@^0.0.7":
|
"@scm-manager/ui-extensions@^0.1.1":
|
||||||
version "0.0.7"
|
version "0.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/@scm-manager/ui-extensions/-/ui-extensions-0.0.7.tgz#a0a657a1410b78838ba0b36096ef631dca7fe27e"
|
resolved "https://registry.yarnpkg.com/@scm-manager/ui-extensions/-/ui-extensions-0.1.1.tgz#966e62d89981e92a14adf7e674e646e76de96d45"
|
||||||
dependencies:
|
dependencies:
|
||||||
react "^16.4.2"
|
react "^16.4.2"
|
||||||
react-dom "^16.4.2"
|
react-dom "^16.4.2"
|
||||||
|
|||||||
@@ -3,16 +3,18 @@
|
|||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"description": "UI Components for SCM-Manager and its plugins",
|
"description": "UI Components for SCM-Manager and its plugins",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"files": ["src"],
|
"files": [
|
||||||
|
"src"
|
||||||
|
],
|
||||||
"repository": "https://bitbucket.org/sdorra/scm-manager",
|
"repository": "https://bitbucket.org/sdorra/scm-manager",
|
||||||
"author": "Sebastian Sdorra <sebastian.sdorra@cloudogu.com>",
|
"author": "Sebastian Sdorra <sebastian.sdorra@cloudogu.com>",
|
||||||
"license" : "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"update-index": "create-index -r src",
|
"update-index": "create-index -r src",
|
||||||
"eslint-fix": "eslint src --fix"
|
"eslint-fix": "eslint src --fix"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@scm-manager/ui-bundler": "^0.0.19",
|
"@scm-manager/ui-bundler": "^0.0.21",
|
||||||
"create-index": "^2.3.0",
|
"create-index": "^2.3.0",
|
||||||
"enzyme": "^3.5.0",
|
"enzyme": "^3.5.0",
|
||||||
"enzyme-adapter-react-16": "^1.3.1",
|
"enzyme-adapter-react-16": "^1.3.1",
|
||||||
@@ -23,23 +25,32 @@
|
|||||||
"react-router-enzyme-context": "^1.2.0"
|
"react-router-enzyme-context": "^1.2.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@scm-manager/ui-extensions": "^0.1.1",
|
||||||
|
"@scm-manager/ui-types": "0.0.1",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"moment": "^2.22.2",
|
"moment": "^2.22.2",
|
||||||
"react": "^16.5.2",
|
"react": "^16.5.2",
|
||||||
"react-dom": "^16.5.2",
|
"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"
|
||||||
"@scm-manager/ui-types": "0.0.1"
|
|
||||||
},
|
},
|
||||||
"browserify": {
|
"browserify": {
|
||||||
"transform": [
|
"transform": [
|
||||||
["browserify-css"],
|
[
|
||||||
|
"browserify-css"
|
||||||
|
],
|
||||||
[
|
[
|
||||||
"babelify",
|
"babelify",
|
||||||
{
|
{
|
||||||
"plugins": ["@babel/plugin-proposal-class-properties"],
|
"plugins": [
|
||||||
"presets": ["@babel/preset-env", "@babel/preset-flow", "@babel/preset-react"]
|
"@babel/plugin-proposal-class-properties"
|
||||||
|
],
|
||||||
|
"presets": [
|
||||||
|
"@babel/preset-env",
|
||||||
|
"@babel/preset-flow",
|
||||||
|
"@babel/preset-react"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
// @flow
|
||||||
|
import React from "react";
|
||||||
|
import { binder } from "@scm-manager/ui-extensions";
|
||||||
|
import { NavLink } from "../navigation";
|
||||||
|
import { Route } from "react-router-dom";
|
||||||
|
import { translate } from "react-i18next";
|
||||||
|
|
||||||
|
class ConfigurationBinder {
|
||||||
|
|
||||||
|
i18nNamespace: string = "plugins";
|
||||||
|
|
||||||
|
bindGlobal(to: string, labelI18nKey: string, linkName: string, ConfigurationComponent: any) {
|
||||||
|
|
||||||
|
// create predicate based on the link name of the index resource
|
||||||
|
// if the linkname is not available, the navigation link and the route are not bound to the extension points
|
||||||
|
const configPredicate = (props: Object) => {
|
||||||
|
return props.links && props.links[linkName];
|
||||||
|
};
|
||||||
|
|
||||||
|
// create NavigationLink with translated label
|
||||||
|
const ConfigNavLink = translate(this.i18nNamespace)(({t}) => {
|
||||||
|
return <NavLink to={"/config" + to} label={t(labelI18nKey)} />;
|
||||||
|
});
|
||||||
|
|
||||||
|
// bind navigation link to extension point
|
||||||
|
binder.bind("config.navigation", ConfigNavLink, configPredicate);
|
||||||
|
|
||||||
|
|
||||||
|
// route for global configuration, passes the link from the index resource to component
|
||||||
|
const ConfigRoute = ({ url, links }) => {
|
||||||
|
const link = links[linkName].href;
|
||||||
|
return <Route path={url + to}
|
||||||
|
render={() => <ConfigurationComponent link={link}/>}
|
||||||
|
exact/>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// bind config route to extension point
|
||||||
|
binder.bind("config.route", ConfigRoute, configPredicate);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new ConfigurationBinder();
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
//@flow
|
||||||
|
import React from "react";
|
||||||
|
import { translate } from "react-i18next";
|
||||||
|
import type { Links } from "@scm-manager/ui-types";
|
||||||
|
import {
|
||||||
|
apiClient,
|
||||||
|
SubmitButton,
|
||||||
|
Loading,
|
||||||
|
ErrorNotification
|
||||||
|
} from "../";
|
||||||
|
|
||||||
|
type RenderProps = {
|
||||||
|
readOnly: boolean,
|
||||||
|
initialConfiguration: Configuration,
|
||||||
|
onConfigurationChange: (Configuration, boolean) => void
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
link: string,
|
||||||
|
render: (props: RenderProps) => any, // ???
|
||||||
|
|
||||||
|
// context props
|
||||||
|
t: (string) => string
|
||||||
|
};
|
||||||
|
|
||||||
|
type Configuration = {
|
||||||
|
_links: Links
|
||||||
|
} & Object;
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
error?: Error,
|
||||||
|
fetching: boolean,
|
||||||
|
modifying: boolean,
|
||||||
|
contentType?: string,
|
||||||
|
|
||||||
|
configuration?: Configuration,
|
||||||
|
modifiedConfiguration?: Configuration,
|
||||||
|
valid: boolean
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GlobalConfiguration uses the render prop pattern to encapsulate the logic for
|
||||||
|
* synchronizing the configuration with the backend.
|
||||||
|
*/
|
||||||
|
class GlobalConfiguration extends React.Component<Props, State> {
|
||||||
|
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
fetching: true,
|
||||||
|
modifying: false,
|
||||||
|
valid: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const { link } = this.props;
|
||||||
|
|
||||||
|
apiClient.get(link)
|
||||||
|
.then(this.captureContentType)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(this.loadConfig)
|
||||||
|
.catch(this.handleError);
|
||||||
|
}
|
||||||
|
|
||||||
|
captureContentType = (response: Response) => {
|
||||||
|
const contentType = response.headers.get("Content-Type");
|
||||||
|
this.setState({
|
||||||
|
contentType
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
|
getContentType = (): string => {
|
||||||
|
const { contentType } = this.state;
|
||||||
|
return contentType ? contentType : "application/json";
|
||||||
|
};
|
||||||
|
|
||||||
|
handleError = (error: Error) => {
|
||||||
|
this.setState({
|
||||||
|
error,
|
||||||
|
fetching: false,
|
||||||
|
modifying: false
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
loadConfig = (configuration: Configuration) => {
|
||||||
|
this.setState({
|
||||||
|
configuration,
|
||||||
|
fetching: false,
|
||||||
|
error: undefined
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
getModificationUrl = (): ?string => {
|
||||||
|
const { configuration } = this.state;
|
||||||
|
if (configuration) {
|
||||||
|
const links = configuration._links;
|
||||||
|
if (links && links.update) {
|
||||||
|
return links.update.href;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
isReadOnly = (): boolean => {
|
||||||
|
const modificationUrl = this.getModificationUrl();
|
||||||
|
return !modificationUrl;
|
||||||
|
};
|
||||||
|
|
||||||
|
configurationChanged = (configuration: Configuration, valid: boolean) => {
|
||||||
|
this.setState({
|
||||||
|
modifiedConfiguration: configuration,
|
||||||
|
valid
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
modifyConfiguration = (event: Event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
this.setState({ modifying: true });
|
||||||
|
|
||||||
|
const {modifiedConfiguration} = this.state;
|
||||||
|
|
||||||
|
apiClient.put(this.getModificationUrl(), modifiedConfiguration, this.getContentType())
|
||||||
|
.then(() => this.setState({ modifying: false }))
|
||||||
|
.catch(this.handleError);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { t } = this.props;
|
||||||
|
const { fetching, error, configuration, modifying, valid } = this.state;
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <ErrorNotification error={error}/>;
|
||||||
|
} else if (fetching || !configuration) {
|
||||||
|
return <Loading />;
|
||||||
|
} else {
|
||||||
|
const readOnly = this.isReadOnly();
|
||||||
|
|
||||||
|
const renderProps: RenderProps = {
|
||||||
|
readOnly,
|
||||||
|
initialConfiguration: configuration,
|
||||||
|
onConfigurationChange: this.configurationChanged
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={this.modifyConfiguration}>
|
||||||
|
{ this.props.render(renderProps) }
|
||||||
|
<hr/>
|
||||||
|
<SubmitButton
|
||||||
|
label={t("config-form.submit")}
|
||||||
|
disabled={!valid || readOnly}
|
||||||
|
loading={modifying}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate("config")(GlobalConfiguration);
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
// @flow
|
||||||
|
export { default as ConfigurationBinder } from "./ConfigurationBinder";
|
||||||
|
export { default as GlobalConfiguration } from "./GlobalConfiguration";
|
||||||
@@ -4,15 +4,16 @@ import { Help } from "../index";
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
label?: string,
|
label?: string,
|
||||||
|
name?: string,
|
||||||
checked: boolean,
|
checked: boolean,
|
||||||
onChange?: boolean => void,
|
onChange?: (value: boolean, name?: string) => void,
|
||||||
disabled?: boolean,
|
disabled?: boolean,
|
||||||
helpText?: string
|
helpText?: string
|
||||||
};
|
};
|
||||||
class Checkbox extends React.Component<Props> {
|
class Checkbox extends React.Component<Props> {
|
||||||
onCheckboxChange = (event: SyntheticInputEvent<HTMLInputElement>) => {
|
onCheckboxChange = (event: SyntheticInputEvent<HTMLInputElement>) => {
|
||||||
if (this.props.onChange) {
|
if (this.props.onChange) {
|
||||||
this.props.onChange(event.target.checked);
|
this.props.onChange(event.target.checked, this.props.name);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5,11 +5,12 @@ import { LabelWithHelpIcon } from "../index";
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
label?: string,
|
label?: string,
|
||||||
|
name?: string,
|
||||||
placeholder?: string,
|
placeholder?: string,
|
||||||
value?: string,
|
value?: string,
|
||||||
type?: string,
|
type?: string,
|
||||||
autofocus?: boolean,
|
autofocus?: boolean,
|
||||||
onChange: string => void,
|
onChange: (value: string, name?: string) => void,
|
||||||
onReturnPressed?: () => void,
|
onReturnPressed?: () => void,
|
||||||
validationError: boolean,
|
validationError: boolean,
|
||||||
errorMessage: string,
|
errorMessage: string,
|
||||||
@@ -32,7 +33,7 @@ class InputField extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleInput = (event: SyntheticInputEvent<HTMLInputElement>) => {
|
handleInput = (event: SyntheticInputEvent<HTMLInputElement>) => {
|
||||||
this.props.onChange(event.target.value);
|
this.props.onChange(event.target.value, this.props.name);
|
||||||
};
|
};
|
||||||
|
|
||||||
handleKeyPress = (event: SyntheticKeyboardEvent<HTMLInputElement>) => {
|
handleKeyPress = (event: SyntheticKeyboardEvent<HTMLInputElement>) => {
|
||||||
|
|||||||
@@ -9,10 +9,11 @@ export type SelectItem = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
name?: string,
|
||||||
label?: string,
|
label?: string,
|
||||||
options: SelectItem[],
|
options: SelectItem[],
|
||||||
value?: SelectItem,
|
value?: string,
|
||||||
onChange: string => void,
|
onChange: (value: string, name?: string) => void,
|
||||||
loading?: boolean,
|
loading?: boolean,
|
||||||
helpText?: string
|
helpText?: string
|
||||||
};
|
};
|
||||||
@@ -29,7 +30,7 @@ class Select extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleInput = (event: SyntheticInputEvent<HTMLSelectElement>) => {
|
handleInput = (event: SyntheticInputEvent<HTMLSelectElement>) => {
|
||||||
this.props.onChange(event.target.value);
|
this.props.onChange(event.target.value, this.props.name);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export { getPageFromMatch } from "./urls";
|
|||||||
export { apiClient, NOT_FOUND_ERROR, UNAUTHORIZED_ERROR } from "./apiclient.js";
|
export { apiClient, NOT_FOUND_ERROR, UNAUTHORIZED_ERROR } from "./apiclient.js";
|
||||||
|
|
||||||
export * from "./buttons";
|
export * from "./buttons";
|
||||||
|
export * from "./config";
|
||||||
export * from "./forms";
|
export * from "./forms";
|
||||||
export * from "./layout";
|
export * from "./layout";
|
||||||
export * from "./modals";
|
export * from "./modals";
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
//@flow
|
//@flow
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import type { Me } from "@scm-manager/ui-types";
|
import type { Me } from "@scm-manager/ui-types";
|
||||||
|
import {Link} from "react-router-dom";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
me?: Me
|
me?: Me
|
||||||
@@ -15,7 +16,7 @@ class Footer extends React.Component<Props> {
|
|||||||
return (
|
return (
|
||||||
<footer className="footer">
|
<footer className="footer">
|
||||||
<div className="container is-centered">
|
<div className="container is-centered">
|
||||||
<p className="has-text-centered">{me.displayName}</p>
|
<p className="has-text-centered"><Link to={"/me"}>{me.displayName}</Link></p>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -641,9 +641,9 @@
|
|||||||
version "0.0.2"
|
version "0.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85"
|
resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85"
|
||||||
|
|
||||||
"@scm-manager/ui-bundler@^0.0.19":
|
"@scm-manager/ui-bundler@^0.0.21":
|
||||||
version "0.0.19"
|
version "0.0.21"
|
||||||
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.19.tgz#646ab1fa1e5389fad614542215c60678fb9816ae"
|
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.21.tgz#f8b5fa355415cc67b8aaf8744e1701a299dff647"
|
||||||
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"
|
||||||
@@ -681,6 +681,13 @@
|
|||||||
vinyl-source-stream "^2.0.0"
|
vinyl-source-stream "^2.0.0"
|
||||||
watchify "^3.11.0"
|
watchify "^3.11.0"
|
||||||
|
|
||||||
|
"@scm-manager/ui-extensions@^0.1.1":
|
||||||
|
version "0.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@scm-manager/ui-extensions/-/ui-extensions-0.1.1.tgz#966e62d89981e92a14adf7e674e646e76de96d45"
|
||||||
|
dependencies:
|
||||||
|
react "^16.4.2"
|
||||||
|
react-dom "^16.4.2"
|
||||||
|
|
||||||
"@types/node@*":
|
"@types/node@*":
|
||||||
version "10.12.0"
|
version "10.12.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.0.tgz#ea6dcbddbc5b584c83f06c60e82736d8fbb0c235"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.0.tgz#ea6dcbddbc5b584c83f06c60e82736d8fbb0c235"
|
||||||
@@ -6229,6 +6236,15 @@ rc@^1.2.7:
|
|||||||
minimist "^1.2.0"
|
minimist "^1.2.0"
|
||||||
strip-json-comments "~2.0.1"
|
strip-json-comments "~2.0.1"
|
||||||
|
|
||||||
|
react-dom@^16.4.2:
|
||||||
|
version "16.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.6.0.tgz#6375b8391e019a632a89a0988bce85f0cc87a92f"
|
||||||
|
dependencies:
|
||||||
|
loose-envify "^1.1.0"
|
||||||
|
object-assign "^4.1.1"
|
||||||
|
prop-types "^15.6.2"
|
||||||
|
scheduler "^0.10.0"
|
||||||
|
|
||||||
react-dom@^16.5.2:
|
react-dom@^16.5.2:
|
||||||
version "16.5.2"
|
version "16.5.2"
|
||||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.5.2.tgz#b69ee47aa20bab5327b2b9d7c1fe2a30f2cfa9d7"
|
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.5.2.tgz#b69ee47aa20bab5327b2b9d7c1fe2a30f2cfa9d7"
|
||||||
@@ -6299,6 +6315,15 @@ react-test-renderer@^16.0.0-0:
|
|||||||
react-is "^16.5.2"
|
react-is "^16.5.2"
|
||||||
schedule "^0.5.0"
|
schedule "^0.5.0"
|
||||||
|
|
||||||
|
react@^16.4.2:
|
||||||
|
version "16.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react/-/react-16.6.0.tgz#b34761cfaf3e30f5508bc732fb4736730b7da246"
|
||||||
|
dependencies:
|
||||||
|
loose-envify "^1.1.0"
|
||||||
|
object-assign "^4.1.1"
|
||||||
|
prop-types "^15.6.2"
|
||||||
|
scheduler "^0.10.0"
|
||||||
|
|
||||||
react@^16.5.2:
|
react@^16.5.2:
|
||||||
version "16.5.2"
|
version "16.5.2"
|
||||||
resolved "https://registry.yarnpkg.com/react/-/react-16.5.2.tgz#19f6b444ed139baa45609eee6dc3d318b3895d42"
|
resolved "https://registry.yarnpkg.com/react/-/react-16.5.2.tgz#19f6b444ed139baa45609eee6dc3d318b3895d42"
|
||||||
@@ -6726,6 +6751,13 @@ schedule@^0.5.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
object-assign "^4.1.1"
|
object-assign "^4.1.1"
|
||||||
|
|
||||||
|
scheduler@^0.10.0:
|
||||||
|
version "0.10.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.10.0.tgz#7988de90fe7edccc774ea175a783e69c40c521e1"
|
||||||
|
dependencies:
|
||||||
|
loose-envify "^1.1.0"
|
||||||
|
object-assign "^4.1.1"
|
||||||
|
|
||||||
"semver@2 || 3 || 4 || 5", semver@^5.0.1, semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1:
|
"semver@2 || 3 || 4 || 5", semver@^5.0.1, semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1:
|
||||||
version "5.6.0"
|
version "5.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004"
|
resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004"
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
"check": "flow check"
|
"check": "flow check"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@scm-manager/ui-bundler": "^0.0.19"
|
"@scm-manager/ui-bundler": "^0.0.21"
|
||||||
},
|
},
|
||||||
"browserify": {
|
"browserify": {
|
||||||
"transform": [
|
"transform": [
|
||||||
|
|||||||
@@ -2,5 +2,6 @@
|
|||||||
|
|
||||||
export type Me = {
|
export type Me = {
|
||||||
name: string,
|
name: string,
|
||||||
displayName: string
|
displayName: string,
|
||||||
|
mail: string
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -707,9 +707,9 @@
|
|||||||
version "0.0.2"
|
version "0.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85"
|
resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85"
|
||||||
|
|
||||||
"@scm-manager/ui-bundler@^0.0.19":
|
"@scm-manager/ui-bundler@^0.0.21":
|
||||||
version "0.0.19"
|
version "0.0.21"
|
||||||
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.19.tgz#646ab1fa1e5389fad614542215c60678fb9816ae"
|
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.21.tgz#f8b5fa355415cc67b8aaf8744e1701a299dff647"
|
||||||
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"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-free": "^5.3.1",
|
"@fortawesome/fontawesome-free": "^5.3.1",
|
||||||
"@scm-manager/ui-extensions": "^0.0.7",
|
"@scm-manager/ui-extensions": "^0.1.1",
|
||||||
"bulma": "^0.7.1",
|
"bulma": "^0.7.1",
|
||||||
"bulma-tooltip": "^2.0.2",
|
"bulma-tooltip": "^2.0.2",
|
||||||
"classnames": "^2.2.5",
|
"classnames": "^2.2.5",
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
"pre-commit": "jest && flow && eslint src"
|
"pre-commit": "jest && flow && eslint src"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@scm-manager/ui-bundler": "^0.0.19",
|
"@scm-manager/ui-bundler": "^0.0.21",
|
||||||
"copyfiles": "^2.0.0",
|
"copyfiles": "^2.0.0",
|
||||||
"enzyme": "^3.3.0",
|
"enzyme": "^3.3.0",
|
||||||
"enzyme-adapter-react-16": "^1.1.1",
|
"enzyme-adapter-react-16": "^1.1.1",
|
||||||
|
|||||||
@@ -38,5 +38,14 @@
|
|||||||
"paginator": {
|
"paginator": {
|
||||||
"next": "Next",
|
"next": "Next",
|
||||||
"previous": "Previous"
|
"previous": "Previous"
|
||||||
|
},
|
||||||
|
"profile": {
|
||||||
|
"actions-label": "Actions",
|
||||||
|
"username": "Username",
|
||||||
|
"displayName": "Display Name",
|
||||||
|
"mail": "E-Mail",
|
||||||
|
"change-password": "Change password",
|
||||||
|
"error-title": "Error",
|
||||||
|
"error-subtitle": "Cannot display profile"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,9 @@
|
|||||||
"edit-user-button": {
|
"edit-user-button": {
|
||||||
"label": "Edit"
|
"label": "Edit"
|
||||||
},
|
},
|
||||||
|
"set-password-button": {
|
||||||
|
"label": "Set password"
|
||||||
|
},
|
||||||
"user-form": {
|
"user-form": {
|
||||||
"submit": "Submit"
|
"submit": "Submit"
|
||||||
},
|
},
|
||||||
@@ -49,8 +52,11 @@
|
|||||||
"name-invalid": "This name is invalid",
|
"name-invalid": "This name is invalid",
|
||||||
"displayname-invalid": "This displayname is invalid",
|
"displayname-invalid": "This displayname is invalid",
|
||||||
"password-invalid": "Password has to be between 6 and 32 characters",
|
"password-invalid": "Password has to be between 6 and 32 characters",
|
||||||
"passwordValidation-invalid": "Passwords have to be the same",
|
"passwordValidation-invalid": "Passwords have to be identical",
|
||||||
"validatePassword": "Please validate password here"
|
"validatePassword": "Confirm password"
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"set-password-successful": "Password successfully set"
|
||||||
},
|
},
|
||||||
"help": {
|
"help": {
|
||||||
"usernameHelpText": "Unique name of the user.",
|
"usernameHelpText": "Unique name of the user.",
|
||||||
|
|||||||
@@ -2,12 +2,19 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { translate } from "react-i18next";
|
import { translate } from "react-i18next";
|
||||||
import { Route } from "react-router";
|
import { Route } from "react-router";
|
||||||
|
import { ExtensionPoint } from "@scm-manager/ui-extensions";
|
||||||
|
|
||||||
|
import type { Links } from "@scm-manager/ui-types";
|
||||||
import { Page, Navigation, NavLink, Section } from "@scm-manager/ui-components";
|
import { Page, Navigation, NavLink, Section } from "@scm-manager/ui-components";
|
||||||
import GlobalConfig from "./GlobalConfig";
|
import GlobalConfig from "./GlobalConfig";
|
||||||
import type { History } from "history";
|
import type { History } from "history";
|
||||||
|
import {connect} from "react-redux";
|
||||||
|
import {compose} from "redux";
|
||||||
|
import { getLinks } from "../../modules/indexResource";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
links: Links,
|
||||||
|
|
||||||
// context objects
|
// context objects
|
||||||
t: string => string,
|
t: string => string,
|
||||||
match: any,
|
match: any,
|
||||||
@@ -27,15 +34,23 @@ class Config extends React.Component<Props> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { t } = this.props;
|
const { links, t } = this.props;
|
||||||
|
|
||||||
const url = this.matchedUrl();
|
const url = this.matchedUrl();
|
||||||
|
const extensionProps = {
|
||||||
|
links,
|
||||||
|
url
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<div className="columns">
|
<div className="columns">
|
||||||
<div className="column is-three-quarters">
|
<div className="column is-three-quarters">
|
||||||
<Route path={url} exact component={GlobalConfig} />
|
<Route path={url} exact component={GlobalConfig} />
|
||||||
|
<ExtensionPoint name="config.route"
|
||||||
|
props={extensionProps}
|
||||||
|
renderAll={true}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="column">
|
<div className="column">
|
||||||
<Navigation>
|
<Navigation>
|
||||||
@@ -44,6 +59,10 @@ class Config extends React.Component<Props> {
|
|||||||
to={`${url}`}
|
to={`${url}`}
|
||||||
label={t("global-config.navigation-label")}
|
label={t("global-config.navigation-label")}
|
||||||
/>
|
/>
|
||||||
|
<ExtensionPoint name="config.navigation"
|
||||||
|
props={extensionProps}
|
||||||
|
renderAll={true}
|
||||||
|
/>
|
||||||
</Section>
|
</Section>
|
||||||
</Navigation>
|
</Navigation>
|
||||||
</div>
|
</div>
|
||||||
@@ -53,4 +72,15 @@ class Config extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default translate("config")(Config);
|
const mapStateToProps = (state: any) => {
|
||||||
|
const links = getLinks(state);
|
||||||
|
return {
|
||||||
|
links
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default compose(
|
||||||
|
connect(mapStateToProps),
|
||||||
|
translate("config")
|
||||||
|
)(Config);
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import SingleGroup from "../groups/containers/SingleGroup";
|
|||||||
import AddGroup from "../groups/containers/AddGroup";
|
import AddGroup from "../groups/containers/AddGroup";
|
||||||
|
|
||||||
import Config from "../config/containers/Config";
|
import Config from "../config/containers/Config";
|
||||||
|
import Profile from "./Profile";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
authenticated?: boolean
|
authenticated?: boolean
|
||||||
@@ -101,11 +102,16 @@ class Main extends React.Component<Props> {
|
|||||||
authenticated={authenticated}
|
authenticated={authenticated}
|
||||||
/>
|
/>
|
||||||
<ProtectedRoute
|
<ProtectedRoute
|
||||||
exact
|
|
||||||
path="/config"
|
path="/config"
|
||||||
component={Config}
|
component={Config}
|
||||||
authenticated={authenticated}
|
authenticated={authenticated}
|
||||||
/>
|
/>
|
||||||
|
<ProtectedRoute
|
||||||
|
exact
|
||||||
|
path="/me"
|
||||||
|
component={Profile}
|
||||||
|
authenticated={authenticated}
|
||||||
|
/>
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
98
scm-ui/src/containers/Profile.js
Normal file
98
scm-ui/src/containers/Profile.js
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
// @flow
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Page,
|
||||||
|
Navigation,
|
||||||
|
Section,
|
||||||
|
MailLink
|
||||||
|
} from "../../../scm-ui-components/packages/ui-components/src/index";
|
||||||
|
import { NavLink } from "react-router-dom";
|
||||||
|
import { getMe } from "../modules/auth";
|
||||||
|
import { compose } from "redux";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { translate } from "react-i18next";
|
||||||
|
import type { Me } from "../../../scm-ui-components/packages/ui-types/src/index";
|
||||||
|
import AvatarWrapper from "../repos/components/changesets/AvatarWrapper";
|
||||||
|
import { ErrorPage } from "@scm-manager/ui-components";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
me: Me,
|
||||||
|
|
||||||
|
// Context props
|
||||||
|
t: string => string
|
||||||
|
};
|
||||||
|
type State = {};
|
||||||
|
|
||||||
|
class Profile extends React.Component<Props, State> {
|
||||||
|
render() {
|
||||||
|
const { me, t } = this.props;
|
||||||
|
|
||||||
|
if (me) {
|
||||||
|
return (
|
||||||
|
<ErrorPage
|
||||||
|
title={t("profile.error-title")}
|
||||||
|
subtitle={t("profile.error-subtitle")}
|
||||||
|
error={{ name: "Error", message: "'me' is undefined" }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Page title={me.displayName}>
|
||||||
|
<div className="columns">
|
||||||
|
<AvatarWrapper>
|
||||||
|
<div>
|
||||||
|
<figure className="media-left">
|
||||||
|
<p className="image is-64x64">
|
||||||
|
{
|
||||||
|
// TODO: add avatar
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
</AvatarWrapper>
|
||||||
|
<div className="column is-two-quarters">
|
||||||
|
<table className="table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>{t("profile.username")}</td>
|
||||||
|
<td>{me.name}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{t("profile.displayName")}</td>
|
||||||
|
<td>{me.displayName}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{t("profile.mail")}</td>
|
||||||
|
<td>
|
||||||
|
<MailLink address={me.mail} />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div className="column is-one-quarter">
|
||||||
|
<Navigation>
|
||||||
|
<Section label={t("profile.actions-label")} />
|
||||||
|
<NavLink to={"me/password"}>
|
||||||
|
{t("profile.change-password")}
|
||||||
|
</NavLink>
|
||||||
|
</Navigation>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = state => {
|
||||||
|
return {
|
||||||
|
me: getMe(state)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default compose(
|
||||||
|
translate("commons"),
|
||||||
|
connect(mapStateToProps)
|
||||||
|
)(Profile);
|
||||||
@@ -2,16 +2,21 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import GroupForm from "../components/GroupForm";
|
import GroupForm from "../components/GroupForm";
|
||||||
import { modifyGroup, fetchGroup } from "../modules/groups";
|
import {
|
||||||
|
modifyGroup,
|
||||||
|
modifyGroupReset,
|
||||||
|
isModifyGroupPending,
|
||||||
|
getModifyGroupFailure
|
||||||
|
} from "../modules/groups";
|
||||||
import type { History } from "history";
|
import type { History } from "history";
|
||||||
import { withRouter } from "react-router-dom";
|
import { withRouter } from "react-router-dom";
|
||||||
import type { Group } from "@scm-manager/ui-types";
|
import type { Group } from "@scm-manager/ui-types";
|
||||||
import { isModifyGroupPending, getModifyGroupFailure } from "../modules/groups";
|
|
||||||
import { ErrorNotification } from "@scm-manager/ui-components";
|
import { ErrorNotification } from "@scm-manager/ui-components";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
group: Group,
|
group: Group,
|
||||||
modifyGroup: (group: Group, callback?: () => void) => void,
|
modifyGroup: (group: Group, callback?: () => void) => void,
|
||||||
|
modifyGroupReset: Group => void,
|
||||||
fetchGroup: (name: string) => void,
|
fetchGroup: (name: string) => void,
|
||||||
history: History,
|
history: History,
|
||||||
loading?: boolean,
|
loading?: boolean,
|
||||||
@@ -19,8 +24,12 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
class EditGroup extends React.Component<Props> {
|
class EditGroup extends React.Component<Props> {
|
||||||
|
componentDidMount() {
|
||||||
|
const { group, modifyGroupReset } = this.props;
|
||||||
|
modifyGroupReset(group);
|
||||||
|
}
|
||||||
|
|
||||||
groupModified = (group: Group) => () => {
|
groupModified = (group: Group) => () => {
|
||||||
this.props.fetchGroup(group.name);
|
|
||||||
this.props.history.push(`/group/${group.name}`);
|
this.props.history.push(`/group/${group.name}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -59,8 +68,8 @@ const mapDispatchToProps = dispatch => {
|
|||||||
modifyGroup: (group: Group, callback?: () => void) => {
|
modifyGroup: (group: Group, callback?: () => void) => {
|
||||||
dispatch(modifyGroup(group, callback));
|
dispatch(modifyGroup(group, callback));
|
||||||
},
|
},
|
||||||
fetchGroup: (name: string) => {
|
modifyGroupReset: (group: Group) => {
|
||||||
dispatch(fetchGroup(name));
|
dispatch(modifyGroupReset(group));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import type { Group } from "@scm-manager/ui-types";
|
|||||||
import type { History } from "history";
|
import type { History } from "history";
|
||||||
import {
|
import {
|
||||||
deleteGroup,
|
deleteGroup,
|
||||||
fetchGroup,
|
fetchGroupByName,
|
||||||
getGroupByName,
|
getGroupByName,
|
||||||
isFetchGroupPending,
|
isFetchGroupPending,
|
||||||
getFetchGroupFailure,
|
getFetchGroupFailure,
|
||||||
@@ -37,7 +37,7 @@ type Props = {
|
|||||||
|
|
||||||
// dispatcher functions
|
// dispatcher functions
|
||||||
deleteGroup: (group: Group, callback?: () => void) => void,
|
deleteGroup: (group: Group, callback?: () => void) => void,
|
||||||
fetchGroup: (string, string) => void,
|
fetchGroupByName: (string, string) => void,
|
||||||
|
|
||||||
// context objects
|
// context objects
|
||||||
t: string => string,
|
t: string => string,
|
||||||
@@ -47,7 +47,7 @@ type Props = {
|
|||||||
|
|
||||||
class SingleGroup extends React.Component<Props> {
|
class SingleGroup extends React.Component<Props> {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.fetchGroup(this.props.groupLink, this.props.name);
|
this.props.fetchGroupByName(this.props.groupLink, this.props.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
stripEndingSlash = (url: string) => {
|
stripEndingSlash = (url: string) => {
|
||||||
@@ -147,8 +147,8 @@ const mapStateToProps = (state, ownProps) => {
|
|||||||
|
|
||||||
const mapDispatchToProps = dispatch => {
|
const mapDispatchToProps = dispatch => {
|
||||||
return {
|
return {
|
||||||
fetchGroup: (link: string, name: string) => {
|
fetchGroupByName: (link: string, name: string) => {
|
||||||
dispatch(fetchGroup(link, name));
|
dispatch(fetchGroupByName(link, name));
|
||||||
},
|
},
|
||||||
deleteGroup: (group: Group, callback?: () => void) => {
|
deleteGroup: (group: Group, callback?: () => void) => {
|
||||||
dispatch(deleteGroup(group, callback));
|
dispatch(deleteGroup(group, callback));
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export const MODIFY_GROUP = "scm/groups/MODIFY_GROUP";
|
|||||||
export const MODIFY_GROUP_PENDING = `${MODIFY_GROUP}_${types.PENDING_SUFFIX}`;
|
export const MODIFY_GROUP_PENDING = `${MODIFY_GROUP}_${types.PENDING_SUFFIX}`;
|
||||||
export const MODIFY_GROUP_SUCCESS = `${MODIFY_GROUP}_${types.SUCCESS_SUFFIX}`;
|
export const MODIFY_GROUP_SUCCESS = `${MODIFY_GROUP}_${types.SUCCESS_SUFFIX}`;
|
||||||
export const MODIFY_GROUP_FAILURE = `${MODIFY_GROUP}_${types.FAILURE_SUFFIX}`;
|
export const MODIFY_GROUP_FAILURE = `${MODIFY_GROUP}_${types.FAILURE_SUFFIX}`;
|
||||||
|
export const MODIFY_GROUP_RESET = `${MODIFY_GROUP}_${types.RESET_SUFFIX}`;
|
||||||
|
|
||||||
export const DELETE_GROUP = "scm/groups/DELETE";
|
export const DELETE_GROUP = "scm/groups/DELETE";
|
||||||
export const DELETE_GROUP_PENDING = `${DELETE_GROUP}_${types.PENDING_SUFFIX}`;
|
export const DELETE_GROUP_PENDING = `${DELETE_GROUP}_${types.PENDING_SUFFIX}`;
|
||||||
@@ -84,12 +85,20 @@ export function fetchGroupsFailure(url: string, error: Error): Action {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//fetch group
|
//fetch group
|
||||||
export function fetchGroup(link: string, name: string) {
|
export function fetchGroupByLink(group: Group) {
|
||||||
|
return fetchGroup(group._links.self.href, group.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchGroupByName(link: string, name: string) {
|
||||||
const groupUrl = link.endsWith("/") ? link + name : link + "/" + name;
|
const groupUrl = link.endsWith("/") ? link + name : link + "/" + name;
|
||||||
|
return fetchGroup(groupUrl, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchGroup(link: string, name: string) {
|
||||||
return function(dispatch: any) {
|
return function(dispatch: any) {
|
||||||
dispatch(fetchGroupPending(name));
|
dispatch(fetchGroupPending(name));
|
||||||
return apiClient
|
return apiClient
|
||||||
.get(groupUrl)
|
.get(link)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
return response.json();
|
return response.json();
|
||||||
})
|
})
|
||||||
@@ -189,6 +198,9 @@ export function modifyGroup(group: Group, callback?: () => void) {
|
|||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.then(() => {
|
||||||
|
dispatch(fetchGroupByLink(group));
|
||||||
|
})
|
||||||
.catch(cause => {
|
.catch(cause => {
|
||||||
dispatch(
|
dispatch(
|
||||||
modifyGroupFailure(
|
modifyGroupFailure(
|
||||||
@@ -227,6 +239,13 @@ export function modifyGroupFailure(group: Group, error: Error): Action {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function modifyGroupReset(group: Group): Action {
|
||||||
|
return {
|
||||||
|
type: MODIFY_GROUP_RESET,
|
||||||
|
itemId: group.name
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
//delete group
|
//delete group
|
||||||
|
|
||||||
export function deleteGroup(group: Group, callback?: () => void) {
|
export function deleteGroup(group: Group, callback?: () => void) {
|
||||||
@@ -361,8 +380,6 @@ function byNamesReducer(state: any = {}, action: any = {}) {
|
|||||||
};
|
};
|
||||||
case FETCH_GROUP_SUCCESS:
|
case FETCH_GROUP_SUCCESS:
|
||||||
return reducerByName(state, action.payload.name, action.payload);
|
return reducerByName(state, action.payload.name, action.payload);
|
||||||
case MODIFY_GROUP_SUCCESS:
|
|
||||||
return reducerByName(state, action.payload.name, action.payload);
|
|
||||||
case DELETE_GROUP_SUCCESS:
|
case DELETE_GROUP_SUCCESS:
|
||||||
const newGroupByNames = deleteGroupInGroupsByNames(
|
const newGroupByNames = deleteGroupInGroupsByNames(
|
||||||
state,
|
state,
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ import reducer, {
|
|||||||
getFetchGroupsFailure,
|
getFetchGroupsFailure,
|
||||||
isFetchGroupsPending,
|
isFetchGroupsPending,
|
||||||
selectListAsCollection,
|
selectListAsCollection,
|
||||||
fetchGroup,
|
fetchGroupByLink,
|
||||||
|
fetchGroupByName,
|
||||||
FETCH_GROUP_PENDING,
|
FETCH_GROUP_PENDING,
|
||||||
FETCH_GROUP_SUCCESS,
|
FETCH_GROUP_SUCCESS,
|
||||||
FETCH_GROUP_FAILURE,
|
FETCH_GROUP_FAILURE,
|
||||||
@@ -46,6 +47,7 @@ import reducer, {
|
|||||||
getCreateGroupLink
|
getCreateGroupLink
|
||||||
} from "./groups";
|
} from "./groups";
|
||||||
const GROUPS_URL = "/api/v2/groups";
|
const GROUPS_URL = "/api/v2/groups";
|
||||||
|
const URL_HUMAN_GROUP = "http://localhost:8081/api/v2/groups/humanGroup";
|
||||||
const URL = "/groups";
|
const URL = "/groups";
|
||||||
|
|
||||||
const error = new Error("You have an error!");
|
const error = new Error("You have an error!");
|
||||||
@@ -59,13 +61,13 @@ const humanGroup = {
|
|||||||
members: ["userZaphod"],
|
members: ["userZaphod"],
|
||||||
_links: {
|
_links: {
|
||||||
self: {
|
self: {
|
||||||
href: "http://localhost:8081/api/v2/groups/humanGroup"
|
href: URL_HUMAN_GROUP
|
||||||
},
|
},
|
||||||
delete: {
|
delete: {
|
||||||
href: "http://localhost:8081/api/v2/groups/humanGroup"
|
href: URL_HUMAN_GROUP
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
href: "http://localhost:8081/api/v2/groups/humanGroup"
|
href: URL_HUMAN_GROUP
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
_embedded: {
|
_embedded: {
|
||||||
@@ -171,11 +173,37 @@ describe("groups fetch()", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should sucessfully fetch single group", () => {
|
it("should sucessfully fetch single group by name", () => {
|
||||||
fetchMock.getOnce(GROUPS_URL + "/humanGroup", humanGroup);
|
fetchMock.getOnce(GROUPS_URL + "/humanGroup", humanGroup);
|
||||||
|
|
||||||
const store = mockStore({});
|
const store = mockStore({});
|
||||||
return store.dispatch(fetchGroup(URL, "humanGroup")).then(() => {
|
return store.dispatch(fetchGroupByName(URL, "humanGroup")).then(() => {
|
||||||
|
const actions = store.getActions();
|
||||||
|
expect(actions[0].type).toEqual(FETCH_GROUP_PENDING);
|
||||||
|
expect(actions[1].type).toEqual(FETCH_GROUP_SUCCESS);
|
||||||
|
expect(actions[1].payload).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail fetching single group by name on HTTP 500", () => {
|
||||||
|
fetchMock.getOnce(GROUPS_URL + "/humanGroup", {
|
||||||
|
status: 500
|
||||||
|
});
|
||||||
|
|
||||||
|
const store = mockStore({});
|
||||||
|
return store.dispatch(fetchGroupByName(URL, "humanGroup")).then(() => {
|
||||||
|
const actions = store.getActions();
|
||||||
|
expect(actions[0].type).toEqual(FETCH_GROUP_PENDING);
|
||||||
|
expect(actions[1].type).toEqual(FETCH_GROUP_FAILURE);
|
||||||
|
expect(actions[1].payload).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should sucessfully fetch single group", () => {
|
||||||
|
fetchMock.getOnce(URL_HUMAN_GROUP, humanGroup);
|
||||||
|
|
||||||
|
const store = mockStore({});
|
||||||
|
return store.dispatch(fetchGroupByLink(humanGroup)).then(() => {
|
||||||
const actions = store.getActions();
|
const actions = store.getActions();
|
||||||
expect(actions[0].type).toEqual(FETCH_GROUP_PENDING);
|
expect(actions[0].type).toEqual(FETCH_GROUP_PENDING);
|
||||||
expect(actions[1].type).toEqual(FETCH_GROUP_SUCCESS);
|
expect(actions[1].type).toEqual(FETCH_GROUP_SUCCESS);
|
||||||
@@ -184,12 +212,12 @@ describe("groups fetch()", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should fail fetching single group on HTTP 500", () => {
|
it("should fail fetching single group on HTTP 500", () => {
|
||||||
fetchMock.getOnce(GROUPS_URL + "/humanGroup", {
|
fetchMock.getOnce(URL_HUMAN_GROUP, {
|
||||||
status: 500
|
status: 500
|
||||||
});
|
});
|
||||||
|
|
||||||
const store = mockStore({});
|
const store = mockStore({});
|
||||||
return store.dispatch(fetchGroup(URL, "humanGroup")).then(() => {
|
return store.dispatch(fetchGroupByLink(humanGroup)).then(() => {
|
||||||
const actions = store.getActions();
|
const actions = store.getActions();
|
||||||
expect(actions[0].type).toEqual(FETCH_GROUP_PENDING);
|
expect(actions[0].type).toEqual(FETCH_GROUP_PENDING);
|
||||||
expect(actions[1].type).toEqual(FETCH_GROUP_FAILURE);
|
expect(actions[1].type).toEqual(FETCH_GROUP_FAILURE);
|
||||||
@@ -244,9 +272,10 @@ describe("groups fetch()", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should successfully modify group", () => {
|
it("should successfully modify group", () => {
|
||||||
fetchMock.putOnce("http://localhost:8081/api/v2/groups/humanGroup", {
|
fetchMock.putOnce(URL_HUMAN_GROUP, {
|
||||||
status: 204
|
status: 204
|
||||||
});
|
});
|
||||||
|
fetchMock.getOnce(URL_HUMAN_GROUP, humanGroup);
|
||||||
|
|
||||||
const store = mockStore({});
|
const store = mockStore({});
|
||||||
|
|
||||||
@@ -254,14 +283,16 @@ describe("groups fetch()", () => {
|
|||||||
const actions = store.getActions();
|
const actions = store.getActions();
|
||||||
expect(actions[0].type).toEqual(MODIFY_GROUP_PENDING);
|
expect(actions[0].type).toEqual(MODIFY_GROUP_PENDING);
|
||||||
expect(actions[1].type).toEqual(MODIFY_GROUP_SUCCESS);
|
expect(actions[1].type).toEqual(MODIFY_GROUP_SUCCESS);
|
||||||
|
expect(actions[2].type).toEqual(FETCH_GROUP_PENDING);
|
||||||
expect(actions[1].payload).toEqual(humanGroup);
|
expect(actions[1].payload).toEqual(humanGroup);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should call the callback after modifying group", () => {
|
it("should call the callback after modifying group", () => {
|
||||||
fetchMock.putOnce("http://localhost:8081/api/v2/groups/humanGroup", {
|
fetchMock.putOnce(URL_HUMAN_GROUP, {
|
||||||
status: 204
|
status: 204
|
||||||
});
|
});
|
||||||
|
fetchMock.getOnce(URL_HUMAN_GROUP, humanGroup);
|
||||||
|
|
||||||
let called = false;
|
let called = false;
|
||||||
const callback = () => {
|
const callback = () => {
|
||||||
@@ -273,12 +304,13 @@ describe("groups fetch()", () => {
|
|||||||
const actions = store.getActions();
|
const actions = store.getActions();
|
||||||
expect(actions[0].type).toEqual(MODIFY_GROUP_PENDING);
|
expect(actions[0].type).toEqual(MODIFY_GROUP_PENDING);
|
||||||
expect(actions[1].type).toEqual(MODIFY_GROUP_SUCCESS);
|
expect(actions[1].type).toEqual(MODIFY_GROUP_SUCCESS);
|
||||||
|
expect(actions[2].type).toEqual(FETCH_GROUP_PENDING);
|
||||||
expect(called).toBe(true);
|
expect(called).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should fail modifying group on HTTP 500", () => {
|
it("should fail modifying group on HTTP 500", () => {
|
||||||
fetchMock.putOnce("http://localhost:8081/api/v2/groups/humanGroup", {
|
fetchMock.putOnce(URL_HUMAN_GROUP, {
|
||||||
status: 500
|
status: 500
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -293,7 +325,7 @@ describe("groups fetch()", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should delete successfully group humanGroup", () => {
|
it("should delete successfully group humanGroup", () => {
|
||||||
fetchMock.deleteOnce("http://localhost:8081/api/v2/groups/humanGroup", {
|
fetchMock.deleteOnce(URL_HUMAN_GROUP, {
|
||||||
status: 204
|
status: 204
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -308,7 +340,7 @@ describe("groups fetch()", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should call the callback, after successful delete", () => {
|
it("should call the callback, after successful delete", () => {
|
||||||
fetchMock.deleteOnce("http://localhost:8081/api/v2/groups/humanGroup", {
|
fetchMock.deleteOnce(URL_HUMAN_GROUP, {
|
||||||
status: 204
|
status: 204
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -324,7 +356,7 @@ describe("groups fetch()", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should fail to delete group humanGroup", () => {
|
it("should fail to delete group humanGroup", () => {
|
||||||
fetchMock.deleteOnce("http://localhost:8081/api/v2/groups/humanGroup", {
|
fetchMock.deleteOnce(URL_HUMAN_GROUP, {
|
||||||
status: 500
|
status: 500
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import type { Me } from "@scm-manager/ui-components";
|
import type { Me } from "@scm-manager/ui-types";
|
||||||
import * as types from "./types";
|
import * as types from "./types";
|
||||||
|
|
||||||
import { apiClient, UNAUTHORIZED_ERROR } from "@scm-manager/ui-components";
|
import { apiClient, UNAUTHORIZED_ERROR } from "@scm-manager/ui-components";
|
||||||
@@ -136,7 +136,11 @@ const callFetchMe = (link: string): Promise<Me> => {
|
|||||||
return response.json();
|
return response.json();
|
||||||
})
|
})
|
||||||
.then(json => {
|
.then(json => {
|
||||||
return { name: json.name, displayName: json.displayName };
|
return {
|
||||||
|
name: json.name,
|
||||||
|
displayName: json.displayName,
|
||||||
|
mail: json.mail
|
||||||
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,11 @@ import {
|
|||||||
FETCH_INDEXRESOURCES_SUCCESS
|
FETCH_INDEXRESOURCES_SUCCESS
|
||||||
} from "./indexResource";
|
} from "./indexResource";
|
||||||
|
|
||||||
const me = { name: "tricia", displayName: "Tricia McMillian" };
|
const me = {
|
||||||
|
name: "tricia",
|
||||||
|
displayName: "Tricia McMillian",
|
||||||
|
mail: "trillian@heartofgold.universe"
|
||||||
|
};
|
||||||
|
|
||||||
describe("auth reducer", () => {
|
describe("auth reducer", () => {
|
||||||
it("should set me and login on successful fetch of me", () => {
|
it("should set me and login on successful fetch of me", () => {
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import type { Repository } from "@scm-manager/ui-types";
|
|||||||
import {
|
import {
|
||||||
modifyRepo,
|
modifyRepo,
|
||||||
isModifyRepoPending,
|
isModifyRepoPending,
|
||||||
getModifyRepoFailure
|
getModifyRepoFailure,
|
||||||
|
modifyRepoReset
|
||||||
} from "../modules/repos";
|
} from "../modules/repos";
|
||||||
import { withRouter } from "react-router-dom";
|
import { withRouter } from "react-router-dom";
|
||||||
import type { History } from "history";
|
import type { History } from "history";
|
||||||
@@ -16,6 +17,7 @@ import { ErrorNotification } from "@scm-manager/ui-components";
|
|||||||
type Props = {
|
type Props = {
|
||||||
repository: Repository,
|
repository: Repository,
|
||||||
modifyRepo: (Repository, () => void) => void,
|
modifyRepo: (Repository, () => void) => void,
|
||||||
|
modifyRepoReset: Repository => void,
|
||||||
loading: boolean,
|
loading: boolean,
|
||||||
error: Error,
|
error: Error,
|
||||||
|
|
||||||
@@ -25,6 +27,10 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
class Edit extends React.Component<Props> {
|
class Edit extends React.Component<Props> {
|
||||||
|
componentDidMount() {
|
||||||
|
const { modifyRepoReset, repository } = this.props;
|
||||||
|
modifyRepoReset(repository);
|
||||||
|
}
|
||||||
repoModified = () => {
|
repoModified = () => {
|
||||||
const { history, repository } = this.props;
|
const { history, repository } = this.props;
|
||||||
history.push(`/repo/${repository.namespace}/${repository.name}`);
|
history.push(`/repo/${repository.namespace}/${repository.name}`);
|
||||||
@@ -61,6 +67,9 @@ const mapDispatchToProps = dispatch => {
|
|||||||
return {
|
return {
|
||||||
modifyRepo: (repo: Repository, callback: () => void) => {
|
modifyRepo: (repo: Repository, callback: () => void) => {
|
||||||
dispatch(modifyRepo(repo, callback));
|
dispatch(modifyRepo(repo, callback));
|
||||||
|
},
|
||||||
|
modifyRepoReset: (repo: Repository) => {
|
||||||
|
dispatch(modifyRepoReset(repo));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,19 +1,32 @@
|
|||||||
//@flow
|
//@flow
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {deleteRepo, fetchRepo, getFetchRepoFailure, getRepository, isFetchRepoPending} from "../modules/repos";
|
import {
|
||||||
|
deleteRepo,
|
||||||
|
fetchRepoByName,
|
||||||
|
getFetchRepoFailure,
|
||||||
|
getRepository,
|
||||||
|
isFetchRepoPending
|
||||||
|
} from "../modules/repos";
|
||||||
|
|
||||||
import {connect} from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import {Route, Switch} from "react-router-dom";
|
import { Route, Switch } from "react-router-dom";
|
||||||
import type {Repository} from "@scm-manager/ui-types";
|
import type { Repository } from "@scm-manager/ui-types";
|
||||||
|
|
||||||
import {ErrorPage, Loading, Navigation, NavLink, Page, Section} from "@scm-manager/ui-components";
|
import {
|
||||||
import {translate} from "react-i18next";
|
ErrorPage,
|
||||||
|
Loading,
|
||||||
|
Navigation,
|
||||||
|
NavLink,
|
||||||
|
Page,
|
||||||
|
Section
|
||||||
|
} from "@scm-manager/ui-components";
|
||||||
|
import { translate } from "react-i18next";
|
||||||
import RepositoryDetails from "../components/RepositoryDetails";
|
import RepositoryDetails from "../components/RepositoryDetails";
|
||||||
import DeleteNavAction from "../components/DeleteNavAction";
|
import DeleteNavAction from "../components/DeleteNavAction";
|
||||||
import Edit from "../containers/Edit";
|
import Edit from "../containers/Edit";
|
||||||
import Permissions from "../permissions/containers/Permissions";
|
import Permissions from "../permissions/containers/Permissions";
|
||||||
|
|
||||||
import type {History} from "history";
|
import type { History } from "history";
|
||||||
import EditNavLink from "../components/EditNavLink";
|
import EditNavLink from "../components/EditNavLink";
|
||||||
|
|
||||||
import BranchRoot from "./ChangesetsRoot";
|
import BranchRoot from "./ChangesetsRoot";
|
||||||
@@ -32,7 +45,7 @@ type Props = {
|
|||||||
repoLink: string,
|
repoLink: string,
|
||||||
|
|
||||||
// dispatch functions
|
// dispatch functions
|
||||||
fetchRepo: (link: string, namespace: string, name: string) => void,
|
fetchRepoByName: (link: string, namespace: string, name: string) => void,
|
||||||
deleteRepo: (repository: Repository, () => void) => void,
|
deleteRepo: (repository: Repository, () => void) => void,
|
||||||
|
|
||||||
// context props
|
// context props
|
||||||
@@ -43,9 +56,9 @@ type Props = {
|
|||||||
|
|
||||||
class RepositoryRoot extends React.Component<Props> {
|
class RepositoryRoot extends React.Component<Props> {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { fetchRepo, namespace, name, repoLink } = this.props;
|
const { fetchRepoByName, namespace, name, repoLink } = this.props;
|
||||||
|
|
||||||
fetchRepo(repoLink, namespace, name);
|
fetchRepoByName(repoLink, namespace, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
stripEndingSlash = (url: string) => {
|
stripEndingSlash = (url: string) => {
|
||||||
@@ -209,8 +222,8 @@ const mapStateToProps = (state, ownProps) => {
|
|||||||
|
|
||||||
const mapDispatchToProps = dispatch => {
|
const mapDispatchToProps = dispatch => {
|
||||||
return {
|
return {
|
||||||
fetchRepo: (link: string, namespace: string, name: string) => {
|
fetchRepoByName: (link: string, namespace: string, name: string) => {
|
||||||
dispatch(fetchRepo(link, namespace, name));
|
dispatch(fetchRepoByName(link, namespace, name));
|
||||||
},
|
},
|
||||||
deleteRepo: (repository: Repository, callback: () => void) => {
|
deleteRepo: (repository: Repository, callback: () => void) => {
|
||||||
dispatch(deleteRepo(repository, callback));
|
dispatch(deleteRepo(repository, callback));
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export const MODIFY_REPO = "scm/repos/MODIFY_REPO";
|
|||||||
export const MODIFY_REPO_PENDING = `${MODIFY_REPO}_${types.PENDING_SUFFIX}`;
|
export const MODIFY_REPO_PENDING = `${MODIFY_REPO}_${types.PENDING_SUFFIX}`;
|
||||||
export const MODIFY_REPO_SUCCESS = `${MODIFY_REPO}_${types.SUCCESS_SUFFIX}`;
|
export const MODIFY_REPO_SUCCESS = `${MODIFY_REPO}_${types.SUCCESS_SUFFIX}`;
|
||||||
export const MODIFY_REPO_FAILURE = `${MODIFY_REPO}_${types.FAILURE_SUFFIX}`;
|
export const MODIFY_REPO_FAILURE = `${MODIFY_REPO}_${types.FAILURE_SUFFIX}`;
|
||||||
|
export const MODIFY_REPO_RESET = `${MODIFY_REPO}_${types.RESET_SUFFIX}`;
|
||||||
|
|
||||||
export const DELETE_REPO = "scm/repos/DELETE_REPO";
|
export const DELETE_REPO = "scm/repos/DELETE_REPO";
|
||||||
export const DELETE_REPO_PENDING = `${DELETE_REPO}_${types.PENDING_SUFFIX}`;
|
export const DELETE_REPO_PENDING = `${DELETE_REPO}_${types.PENDING_SUFFIX}`;
|
||||||
@@ -99,13 +100,20 @@ export function fetchReposFailure(err: Error): Action {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// fetch repo
|
// fetch repo
|
||||||
|
export function fetchRepoByLink(repo: Repository) {
|
||||||
|
return fetchRepo(repo._links.self.href, repo.namespace, repo.name);
|
||||||
|
}
|
||||||
|
|
||||||
export function fetchRepo(link: string, namespace: string, name: string) {
|
export function fetchRepoByName(link: string, namespace: string, name: string) {
|
||||||
const repoUrl = link.endsWith("/") ? link : link + "/";
|
const repoUrl = link.endsWith("/") ? link : link + "/";
|
||||||
|
return fetchRepo(`${repoUrl}${namespace}/${name}`, namespace, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchRepo(link: string, namespace: string, name: string) {
|
||||||
return function(dispatch: any) {
|
return function(dispatch: any) {
|
||||||
dispatch(fetchRepoPending(namespace, name));
|
dispatch(fetchRepoPending(namespace, name));
|
||||||
return apiClient
|
return apiClient
|
||||||
.get(`${repoUrl}${namespace}/${name}`)
|
.get(link)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(repository => {
|
.then(repository => {
|
||||||
dispatch(fetchRepoSuccess(repository));
|
dispatch(fetchRepoSuccess(repository));
|
||||||
@@ -213,6 +221,9 @@ export function modifyRepo(repository: Repository, callback?: () => void) {
|
|||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.then(() => {
|
||||||
|
dispatch(fetchRepoByLink(repository));
|
||||||
|
})
|
||||||
.catch(cause => {
|
.catch(cause => {
|
||||||
const error = new Error(`failed to modify repo: ${cause.message}`);
|
const error = new Error(`failed to modify repo: ${cause.message}`);
|
||||||
dispatch(modifyRepoFailure(repository, error));
|
dispatch(modifyRepoFailure(repository, error));
|
||||||
@@ -247,6 +258,14 @@ export function modifyRepoFailure(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function modifyRepoReset(repository: Repository): Action {
|
||||||
|
return {
|
||||||
|
type: MODIFY_REPO_RESET,
|
||||||
|
payload: { repository },
|
||||||
|
itemId: createIdentifier(repository)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// delete
|
// delete
|
||||||
|
|
||||||
export function deleteRepo(repository: Repository, callback?: () => void) {
|
export function deleteRepo(repository: Repository, callback?: () => void) {
|
||||||
@@ -347,8 +366,6 @@ export default function reducer(
|
|||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case FETCH_REPOS_SUCCESS:
|
case FETCH_REPOS_SUCCESS:
|
||||||
return normalizeByNamespaceAndName(action.payload);
|
return normalizeByNamespaceAndName(action.payload);
|
||||||
case MODIFY_REPO_SUCCESS:
|
|
||||||
return reducerByNames(state, action.payload);
|
|
||||||
case FETCH_REPO_SUCCESS:
|
case FETCH_REPO_SUCCESS:
|
||||||
return reducerByNames(state, action.payload);
|
return reducerByNames(state, action.payload);
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ import reducer, {
|
|||||||
fetchReposByLink,
|
fetchReposByLink,
|
||||||
fetchReposByPage,
|
fetchReposByPage,
|
||||||
FETCH_REPO,
|
FETCH_REPO,
|
||||||
fetchRepo,
|
fetchRepoByLink,
|
||||||
|
fetchRepoByName,
|
||||||
FETCH_REPO_PENDING,
|
FETCH_REPO_PENDING,
|
||||||
FETCH_REPO_SUCCESS,
|
FETCH_REPO_SUCCESS,
|
||||||
FETCH_REPO_FAILURE,
|
FETCH_REPO_FAILURE,
|
||||||
@@ -45,7 +46,6 @@ import reducer, {
|
|||||||
MODIFY_REPO,
|
MODIFY_REPO,
|
||||||
isModifyRepoPending,
|
isModifyRepoPending,
|
||||||
getModifyRepoFailure,
|
getModifyRepoFailure,
|
||||||
modifyRepoSuccess,
|
|
||||||
getPermissionsLink
|
getPermissionsLink
|
||||||
} from "./repos";
|
} from "./repos";
|
||||||
import type { Repository, RepositoryCollection } from "@scm-manager/ui-types";
|
import type { Repository, RepositoryCollection } from "@scm-manager/ui-types";
|
||||||
@@ -323,7 +323,7 @@ describe("repos fetch", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should successfully fetch repo slarti/fjords", () => {
|
it("should successfully fetch repo slarti/fjords by name", () => {
|
||||||
fetchMock.getOnce(REPOS_URL + "/slarti/fjords", slartiFjords);
|
fetchMock.getOnce(REPOS_URL + "/slarti/fjords", slartiFjords);
|
||||||
|
|
||||||
const expectedActions = [
|
const expectedActions = [
|
||||||
@@ -343,18 +343,66 @@ describe("repos fetch", () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const store = mockStore({});
|
const store = mockStore({});
|
||||||
return store.dispatch(fetchRepo(URL, "slarti", "fjords")).then(() => {
|
return store.dispatch(fetchRepoByName(URL, "slarti", "fjords")).then(() => {
|
||||||
expect(store.getActions()).toEqual(expectedActions);
|
expect(store.getActions()).toEqual(expectedActions);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should dispatch FETCH_REPO_FAILURE, it the request for slarti/fjords fails", () => {
|
it("should dispatch FETCH_REPO_FAILURE, if the request for slarti/fjords by name fails", () => {
|
||||||
fetchMock.getOnce(REPOS_URL + "/slarti/fjords", {
|
fetchMock.getOnce(REPOS_URL + "/slarti/fjords", {
|
||||||
status: 500
|
status: 500
|
||||||
});
|
});
|
||||||
|
|
||||||
const store = mockStore({});
|
const store = mockStore({});
|
||||||
return store.dispatch(fetchRepo(URL, "slarti", "fjords")).then(() => {
|
return store.dispatch(fetchRepoByName(URL, "slarti", "fjords")).then(() => {
|
||||||
|
const actions = store.getActions();
|
||||||
|
expect(actions[0].type).toEqual(FETCH_REPO_PENDING);
|
||||||
|
expect(actions[1].type).toEqual(FETCH_REPO_FAILURE);
|
||||||
|
expect(actions[1].payload.namespace).toBe("slarti");
|
||||||
|
expect(actions[1].payload.name).toBe("fjords");
|
||||||
|
expect(actions[1].payload.error).toBeDefined();
|
||||||
|
expect(actions[1].itemId).toBe("slarti/fjords");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should successfully fetch repo slarti/fjords", () => {
|
||||||
|
fetchMock.getOnce(
|
||||||
|
"http://localhost:8081/api/v2/repositories/slarti/fjords",
|
||||||
|
slartiFjords
|
||||||
|
);
|
||||||
|
|
||||||
|
const expectedActions = [
|
||||||
|
{
|
||||||
|
type: FETCH_REPO_PENDING,
|
||||||
|
payload: {
|
||||||
|
namespace: "slarti",
|
||||||
|
name: "fjords"
|
||||||
|
},
|
||||||
|
itemId: "slarti/fjords"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: FETCH_REPO_SUCCESS,
|
||||||
|
payload: slartiFjords,
|
||||||
|
itemId: "slarti/fjords"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const store = mockStore({});
|
||||||
|
return store.dispatch(fetchRepoByLink(slartiFjords)).then(() => {
|
||||||
|
expect(store.getActions()).toEqual(expectedActions);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should dispatch FETCH_REPO_FAILURE, it the request for slarti/fjords fails", () => {
|
||||||
|
fetchMock.getOnce(
|
||||||
|
"http://localhost:8081/api/v2/repositories/slarti/fjords",
|
||||||
|
{
|
||||||
|
status: 500
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const store = mockStore({});
|
||||||
|
return store.dispatch(fetchRepoByLink(slartiFjords)).then(() => {
|
||||||
const actions = store.getActions();
|
const actions = store.getActions();
|
||||||
expect(actions[0].type).toEqual(FETCH_REPO_PENDING);
|
expect(actions[0].type).toEqual(FETCH_REPO_PENDING);
|
||||||
expect(actions[1].type).toEqual(FETCH_REPO_FAILURE);
|
expect(actions[1].type).toEqual(FETCH_REPO_FAILURE);
|
||||||
@@ -485,6 +533,12 @@ describe("repos fetch", () => {
|
|||||||
fetchMock.putOnce(slartiFjords._links.update.href, {
|
fetchMock.putOnce(slartiFjords._links.update.href, {
|
||||||
status: 204
|
status: 204
|
||||||
});
|
});
|
||||||
|
fetchMock.getOnce(
|
||||||
|
"http://localhost:8081/api/v2/repositories/slarti/fjords",
|
||||||
|
{
|
||||||
|
status: 500
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
let editedFjords = { ...slartiFjords };
|
let editedFjords = { ...slartiFjords };
|
||||||
editedFjords.description = "coast of africa";
|
editedFjords.description = "coast of africa";
|
||||||
@@ -495,6 +549,7 @@ describe("repos fetch", () => {
|
|||||||
const actions = store.getActions();
|
const actions = store.getActions();
|
||||||
expect(actions[0].type).toEqual(MODIFY_REPO_PENDING);
|
expect(actions[0].type).toEqual(MODIFY_REPO_PENDING);
|
||||||
expect(actions[1].type).toEqual(MODIFY_REPO_SUCCESS);
|
expect(actions[1].type).toEqual(MODIFY_REPO_SUCCESS);
|
||||||
|
expect(actions[2].type).toEqual(FETCH_REPO_PENDING);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -502,6 +557,12 @@ describe("repos fetch", () => {
|
|||||||
fetchMock.putOnce(slartiFjords._links.update.href, {
|
fetchMock.putOnce(slartiFjords._links.update.href, {
|
||||||
status: 204
|
status: 204
|
||||||
});
|
});
|
||||||
|
fetchMock.getOnce(
|
||||||
|
"http://localhost:8081/api/v2/repositories/slarti/fjords",
|
||||||
|
{
|
||||||
|
status: 500
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
let editedFjords = { ...slartiFjords };
|
let editedFjords = { ...slartiFjords };
|
||||||
editedFjords.description = "coast of africa";
|
editedFjords.description = "coast of africa";
|
||||||
@@ -517,6 +578,7 @@ describe("repos fetch", () => {
|
|||||||
const actions = store.getActions();
|
const actions = store.getActions();
|
||||||
expect(actions[0].type).toEqual(MODIFY_REPO_PENDING);
|
expect(actions[0].type).toEqual(MODIFY_REPO_PENDING);
|
||||||
expect(actions[1].type).toEqual(MODIFY_REPO_SUCCESS);
|
expect(actions[1].type).toEqual(MODIFY_REPO_SUCCESS);
|
||||||
|
expect(actions[2].type).toEqual(FETCH_REPO_PENDING);
|
||||||
expect(called).toBe(true);
|
expect(called).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -574,18 +636,6 @@ describe("repos reducer", () => {
|
|||||||
const newState = reducer({}, fetchRepoSuccess(slartiFjords));
|
const newState = reducer({}, fetchRepoSuccess(slartiFjords));
|
||||||
expect(newState.byNames["slarti/fjords"]).toBe(slartiFjords);
|
expect(newState.byNames["slarti/fjords"]).toBe(slartiFjords);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should update reposByNames", () => {
|
|
||||||
const oldState = {
|
|
||||||
byNames: {
|
|
||||||
"slarti/fjords": slartiFjords
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let slartiFjordsEdited = { ...slartiFjords };
|
|
||||||
slartiFjordsEdited.description = "I bless the rains down in Africa";
|
|
||||||
const newState = reducer(oldState, modifyRepoSuccess(slartiFjordsEdited));
|
|
||||||
expect(newState.byNames["slarti/fjords"]).toEqual(slartiFjordsEdited);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("repos selectors", () => {
|
describe("repos selectors", () => {
|
||||||
|
|||||||
177
scm-ui/src/users/components/SetUserPassword.js
Normal file
177
scm-ui/src/users/components/SetUserPassword.js
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
// @flow
|
||||||
|
import React from "react";
|
||||||
|
import type { User } from "@scm-manager/ui-types";
|
||||||
|
import {
|
||||||
|
InputField,
|
||||||
|
SubmitButton,
|
||||||
|
Notification,
|
||||||
|
ErrorNotification
|
||||||
|
} from "@scm-manager/ui-components";
|
||||||
|
import * as userValidator from "./userValidation";
|
||||||
|
import { translate } from "react-i18next";
|
||||||
|
import { updatePassword } from "./updatePassword";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
user: User,
|
||||||
|
t: string => string
|
||||||
|
};
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
password: string,
|
||||||
|
loading: boolean,
|
||||||
|
passwordConfirmationError: boolean,
|
||||||
|
validatePasswordError: boolean,
|
||||||
|
validatePassword: string,
|
||||||
|
error?: Error,
|
||||||
|
passwordChanged: boolean
|
||||||
|
};
|
||||||
|
|
||||||
|
class SetUserPassword extends React.Component<Props, State> {
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
password: "",
|
||||||
|
loading: false,
|
||||||
|
passwordConfirmationError: false,
|
||||||
|
validatePasswordError: false,
|
||||||
|
validatePassword: "",
|
||||||
|
passwordChanged: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
passwordIsValid = () => {
|
||||||
|
return !(
|
||||||
|
this.state.validatePasswordError || this.state.passwordConfirmationError
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
setLoadingState = () => {
|
||||||
|
this.setState({
|
||||||
|
...this.state,
|
||||||
|
loading: true
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
setErrorState = (error: Error) => {
|
||||||
|
this.setState({
|
||||||
|
...this.state,
|
||||||
|
error: error,
|
||||||
|
loading: false
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
setSuccessfulState = () => {
|
||||||
|
this.setState({
|
||||||
|
...this.state,
|
||||||
|
loading: false,
|
||||||
|
passwordChanged: true,
|
||||||
|
password: "",
|
||||||
|
validatePassword: "",
|
||||||
|
validatePasswordError: false,
|
||||||
|
passwordConfirmationError: false
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
submit = (event: Event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (this.passwordIsValid()) {
|
||||||
|
const { user } = this.props;
|
||||||
|
const { password } = this.state;
|
||||||
|
this.setLoadingState();
|
||||||
|
updatePassword(user._links.password.href, password)
|
||||||
|
.then(result => {
|
||||||
|
if (result.error) {
|
||||||
|
this.setErrorState(result.error);
|
||||||
|
} else {
|
||||||
|
this.setSuccessfulState();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { t } = this.props;
|
||||||
|
const { loading, passwordChanged, error } = this.state;
|
||||||
|
|
||||||
|
let message = null;
|
||||||
|
|
||||||
|
if (passwordChanged) {
|
||||||
|
message = (
|
||||||
|
<Notification
|
||||||
|
type={"success"}
|
||||||
|
children={t("password.set-password-successful")}
|
||||||
|
onClose={() => this.onClose()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (error) {
|
||||||
|
message = <ErrorNotification error={error} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={this.submit}>
|
||||||
|
{message}
|
||||||
|
<InputField
|
||||||
|
label={t("user.password")}
|
||||||
|
type="password"
|
||||||
|
onChange={this.handlePasswordChange}
|
||||||
|
value={this.state.password ? this.state.password : ""}
|
||||||
|
validationError={this.state.validatePasswordError}
|
||||||
|
errorMessage={t("validation.password-invalid")}
|
||||||
|
helpText={t("help.passwordHelpText")}
|
||||||
|
/>
|
||||||
|
<InputField
|
||||||
|
label={t("validation.validatePassword")}
|
||||||
|
type="password"
|
||||||
|
onChange={this.handlePasswordValidationChange}
|
||||||
|
value={this.state ? this.state.validatePassword : ""}
|
||||||
|
validationError={this.state.passwordConfirmationError}
|
||||||
|
errorMessage={t("validation.passwordValidation-invalid")}
|
||||||
|
helpText={t("help.passwordConfirmHelpText")}
|
||||||
|
/>
|
||||||
|
<SubmitButton
|
||||||
|
disabled={!this.passwordIsValid()}
|
||||||
|
loading={loading}
|
||||||
|
label={t("user-form.submit")}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePasswordChange = (password: string) => {
|
||||||
|
const validatePasswordError = !this.checkPasswords(
|
||||||
|
password,
|
||||||
|
this.state.validatePassword
|
||||||
|
);
|
||||||
|
this.setState({
|
||||||
|
validatePasswordError: !userValidator.isPasswordValid(password),
|
||||||
|
passwordConfirmationError: validatePasswordError,
|
||||||
|
password: password
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
handlePasswordValidationChange = (validatePassword: string) => {
|
||||||
|
const passwordConfirmed = this.checkPasswords(
|
||||||
|
this.state.password,
|
||||||
|
validatePassword
|
||||||
|
);
|
||||||
|
this.setState({
|
||||||
|
validatePassword,
|
||||||
|
passwordConfirmationError: !passwordConfirmed
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
checkPasswords = (password1: string, password2: string) => {
|
||||||
|
return password1 === password2;
|
||||||
|
};
|
||||||
|
|
||||||
|
onClose = () => {
|
||||||
|
this.setState({
|
||||||
|
...this.state,
|
||||||
|
passwordChanged: false
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate("users")(SetUserPassword);
|
||||||
@@ -22,7 +22,7 @@ type State = {
|
|||||||
mailValidationError: boolean,
|
mailValidationError: boolean,
|
||||||
nameValidationError: boolean,
|
nameValidationError: boolean,
|
||||||
displayNameValidationError: boolean,
|
displayNameValidationError: boolean,
|
||||||
passwordValidationError: boolean,
|
passwordConfirmationError: boolean,
|
||||||
validatePasswordError: boolean,
|
validatePasswordError: boolean,
|
||||||
validatePassword: string
|
validatePassword: string
|
||||||
};
|
};
|
||||||
@@ -44,7 +44,7 @@ class UserForm extends React.Component<Props, State> {
|
|||||||
mailValidationError: false,
|
mailValidationError: false,
|
||||||
displayNameValidationError: false,
|
displayNameValidationError: false,
|
||||||
nameValidationError: false,
|
nameValidationError: false,
|
||||||
passwordValidationError: false,
|
passwordConfirmationError: false,
|
||||||
validatePasswordError: false,
|
validatePasswordError: false,
|
||||||
validatePassword: ""
|
validatePassword: ""
|
||||||
};
|
};
|
||||||
@@ -70,7 +70,7 @@ class UserForm extends React.Component<Props, State> {
|
|||||||
this.state.validatePasswordError ||
|
this.state.validatePasswordError ||
|
||||||
this.state.nameValidationError ||
|
this.state.nameValidationError ||
|
||||||
this.state.mailValidationError ||
|
this.state.mailValidationError ||
|
||||||
this.state.passwordValidationError ||
|
this.state.passwordConfirmationError ||
|
||||||
this.state.displayNameValidationError ||
|
this.state.displayNameValidationError ||
|
||||||
this.isFalsy(user.name) ||
|
this.isFalsy(user.name) ||
|
||||||
this.isFalsy(user.displayName)
|
this.isFalsy(user.displayName)
|
||||||
@@ -89,6 +89,7 @@ class UserForm extends React.Component<Props, State> {
|
|||||||
const user = this.state.user;
|
const user = this.state.user;
|
||||||
|
|
||||||
let nameField = null;
|
let nameField = null;
|
||||||
|
let passwordFields = null;
|
||||||
if (!this.props.user) {
|
if (!this.props.user) {
|
||||||
nameField = (
|
nameField = (
|
||||||
<InputField
|
<InputField
|
||||||
@@ -100,6 +101,28 @@ class UserForm extends React.Component<Props, State> {
|
|||||||
helpText={t("help.usernameHelpText")}
|
helpText={t("help.usernameHelpText")}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
passwordFields = (
|
||||||
|
<>
|
||||||
|
<InputField
|
||||||
|
label={t("user.password")}
|
||||||
|
type="password"
|
||||||
|
onChange={this.handlePasswordChange}
|
||||||
|
value={user ? user.password : ""}
|
||||||
|
validationError={this.state.validatePasswordError}
|
||||||
|
errorMessage={t("validation.password-invalid")}
|
||||||
|
helpText={t("help.passwordHelpText")}
|
||||||
|
/>
|
||||||
|
<InputField
|
||||||
|
label={t("validation.validatePassword")}
|
||||||
|
type="password"
|
||||||
|
onChange={this.handlePasswordValidationChange}
|
||||||
|
value={this.state ? this.state.validatePassword : ""}
|
||||||
|
validationError={this.state.passwordConfirmationError}
|
||||||
|
errorMessage={t("validation.passwordValidation-invalid")}
|
||||||
|
helpText={t("help.passwordConfirmHelpText")}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<form onSubmit={this.submit}>
|
<form onSubmit={this.submit}>
|
||||||
@@ -120,24 +143,7 @@ class UserForm extends React.Component<Props, State> {
|
|||||||
errorMessage={t("validation.mail-invalid")}
|
errorMessage={t("validation.mail-invalid")}
|
||||||
helpText={t("help.mailHelpText")}
|
helpText={t("help.mailHelpText")}
|
||||||
/>
|
/>
|
||||||
<InputField
|
{passwordFields}
|
||||||
label={t("user.password")}
|
|
||||||
type="password"
|
|
||||||
onChange={this.handlePasswordChange}
|
|
||||||
value={user ? user.password : ""}
|
|
||||||
validationError={this.state.validatePasswordError}
|
|
||||||
errorMessage={t("validation.password-invalid")}
|
|
||||||
helpText={t("help.passwordHelpText")}
|
|
||||||
/>
|
|
||||||
<InputField
|
|
||||||
label={t("validation.validatePassword")}
|
|
||||||
type="password"
|
|
||||||
onChange={this.handlePasswordValidationChange}
|
|
||||||
value={this.state ? this.state.validatePassword : ""}
|
|
||||||
validationError={this.state.passwordValidationError}
|
|
||||||
errorMessage={t("validation.passwordValidation-invalid")}
|
|
||||||
helpText={t("help.passwordConfirmHelpText")}
|
|
||||||
/>
|
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label={t("user.admin")}
|
label={t("user.admin")}
|
||||||
onChange={this.handleAdminChange}
|
onChange={this.handleAdminChange}
|
||||||
@@ -189,7 +195,7 @@ class UserForm extends React.Component<Props, State> {
|
|||||||
);
|
);
|
||||||
this.setState({
|
this.setState({
|
||||||
validatePasswordError: !userValidator.isPasswordValid(password),
|
validatePasswordError: !userValidator.isPasswordValid(password),
|
||||||
passwordValidationError: validatePasswordError,
|
passwordConfirmationError: validatePasswordError,
|
||||||
user: { ...this.state.user, password }
|
user: { ...this.state.user, password }
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -201,7 +207,7 @@ class UserForm extends React.Component<Props, State> {
|
|||||||
);
|
);
|
||||||
this.setState({
|
this.setState({
|
||||||
validatePassword,
|
validatePassword,
|
||||||
passwordValidationError: !validatePasswordError
|
passwordConfirmationError: !validatePasswordError
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
28
scm-ui/src/users/components/navLinks/SetPasswordNavLink.js
Normal file
28
scm-ui/src/users/components/navLinks/SetPasswordNavLink.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
//@flow
|
||||||
|
import React from "react";
|
||||||
|
import { translate } from "react-i18next";
|
||||||
|
import type { User } from "@scm-manager/ui-types";
|
||||||
|
import { NavLink } from "@scm-manager/ui-components";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
t: string => string,
|
||||||
|
user: User,
|
||||||
|
passwordUrl: String
|
||||||
|
};
|
||||||
|
|
||||||
|
class ChangePasswordNavLink extends React.Component<Props> {
|
||||||
|
render() {
|
||||||
|
const { t, passwordUrl } = this.props;
|
||||||
|
|
||||||
|
if (!this.hasPermissionToSetPassword()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return <NavLink label={t("set-password-button.label")} to={passwordUrl} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasPermissionToSetPassword = () => {
|
||||||
|
return this.props.user._links.password;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate("users")(ChangePasswordNavLink);
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { shallow } from "enzyme";
|
||||||
|
import "../../../tests/enzyme";
|
||||||
|
import "../../../tests/i18n";
|
||||||
|
import ChangePasswordNavLink from "./SetPasswordNavLink";
|
||||||
|
|
||||||
|
it("should render nothing, if the password link is missing", () => {
|
||||||
|
const user = {
|
||||||
|
_links: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
const navLink = shallow(
|
||||||
|
<ChangePasswordNavLink user={user} passwordUrl="/user/password" />
|
||||||
|
);
|
||||||
|
expect(navLink.text()).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render the navLink", () => {
|
||||||
|
const user = {
|
||||||
|
_links: {
|
||||||
|
password: {
|
||||||
|
href: "/password"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const navLink = shallow(
|
||||||
|
<ChangePasswordNavLink user={user} passwordUrl="/user/password" />
|
||||||
|
);
|
||||||
|
expect(navLink.text()).not.toBe("");
|
||||||
|
});
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
export { default as DeleteUserNavLink } from "./DeleteUserNavLink";
|
export { default as DeleteUserNavLink } from "./DeleteUserNavLink";
|
||||||
export { default as EditUserNavLink } from "./EditUserNavLink";
|
export { default as EditUserNavLink } from "./EditUserNavLink";
|
||||||
|
export { default as SetPasswordNavLink } from "./SetPasswordNavLink";
|
||||||
|
|||||||
15
scm-ui/src/users/components/updatePassword.js
Normal file
15
scm-ui/src/users/components/updatePassword.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
//@flow
|
||||||
|
import { apiClient } from "@scm-manager/ui-components";
|
||||||
|
const CONTENT_TYPE_PASSWORD_OVERWRITE =
|
||||||
|
"application/vnd.scmm-passwordOverwrite+json;v=2";
|
||||||
|
|
||||||
|
export function updatePassword(url: string, password: string) {
|
||||||
|
return apiClient
|
||||||
|
.put(url, { newPassword: password }, CONTENT_TYPE_PASSWORD_OVERWRITE)
|
||||||
|
.then(response => {
|
||||||
|
return response;
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
return { error: err };
|
||||||
|
});
|
||||||
|
}
|
||||||
23
scm-ui/src/users/components/updatePassword.test.js
Normal file
23
scm-ui/src/users/components/updatePassword.test.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
//@flow
|
||||||
|
import fetchMock from "fetch-mock";
|
||||||
|
import { updatePassword } from "./updatePassword";
|
||||||
|
|
||||||
|
describe("get content type", () => {
|
||||||
|
const PASSWORD_URL = "/users/testuser/password";
|
||||||
|
const password = "testpw123";
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fetchMock.reset();
|
||||||
|
fetchMock.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update password", done => {
|
||||||
|
|
||||||
|
fetchMock.put("/api/v2" + PASSWORD_URL, 204);
|
||||||
|
|
||||||
|
updatePassword(PASSWORD_URL, password).then(content => {
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -7,7 +7,8 @@ import type { User } from "@scm-manager/ui-types";
|
|||||||
import {
|
import {
|
||||||
modifyUser,
|
modifyUser,
|
||||||
isModifyUserPending,
|
isModifyUserPending,
|
||||||
getModifyUserFailure
|
getModifyUserFailure,
|
||||||
|
modifyUserReset
|
||||||
} from "../modules/users";
|
} from "../modules/users";
|
||||||
import type { History } from "history";
|
import type { History } from "history";
|
||||||
import { ErrorNotification } from "@scm-manager/ui-components";
|
import { ErrorNotification } from "@scm-manager/ui-components";
|
||||||
@@ -18,6 +19,7 @@ type Props = {
|
|||||||
|
|
||||||
// dispatch functions
|
// dispatch functions
|
||||||
modifyUser: (user: User, callback?: () => void) => void,
|
modifyUser: (user: User, callback?: () => void) => void,
|
||||||
|
modifyUserReset: User => void,
|
||||||
|
|
||||||
// context objects
|
// context objects
|
||||||
user: User,
|
user: User,
|
||||||
@@ -25,6 +27,10 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
class EditUser extends React.Component<Props> {
|
class EditUser extends React.Component<Props> {
|
||||||
|
componentDidMount() {
|
||||||
|
const { modifyUserReset, user } = this.props;
|
||||||
|
modifyUserReset(user);
|
||||||
|
}
|
||||||
userModified = (user: User) => () => {
|
userModified = (user: User) => () => {
|
||||||
this.props.history.push(`/user/${user.name}`);
|
this.props.history.push(`/user/${user.name}`);
|
||||||
};
|
};
|
||||||
@@ -52,6 +58,9 @@ const mapDispatchToProps = dispatch => {
|
|||||||
return {
|
return {
|
||||||
modifyUser: (user: User, callback?: () => void) => {
|
modifyUser: (user: User, callback?: () => void) => {
|
||||||
dispatch(modifyUser(user, callback));
|
dispatch(modifyUser(user, callback));
|
||||||
|
},
|
||||||
|
modifyUserReset: (user: User) => {
|
||||||
|
dispatch(modifyUserReset(user));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import EditUser from "./EditUser";
|
|||||||
import type { User } from "@scm-manager/ui-types";
|
import type { User } from "@scm-manager/ui-types";
|
||||||
import type { History } from "history";
|
import type { History } from "history";
|
||||||
import {
|
import {
|
||||||
fetchUser,
|
fetchUserByName,
|
||||||
deleteUser,
|
deleteUser,
|
||||||
getUserByName,
|
getUserByName,
|
||||||
isFetchUserPending,
|
isFetchUserPending,
|
||||||
@@ -24,9 +24,14 @@ import {
|
|||||||
getDeleteUserFailure
|
getDeleteUserFailure
|
||||||
} from "../modules/users";
|
} from "../modules/users";
|
||||||
|
|
||||||
import { DeleteUserNavLink, EditUserNavLink } from "./../components/navLinks";
|
import {
|
||||||
|
DeleteUserNavLink,
|
||||||
|
EditUserNavLink,
|
||||||
|
SetPasswordNavLink
|
||||||
|
} from "./../components/navLinks";
|
||||||
import { translate } from "react-i18next";
|
import { translate } from "react-i18next";
|
||||||
import { getUsersLink } from "../../modules/indexResource";
|
import { getUsersLink } from "../../modules/indexResource";
|
||||||
|
import SetUserPassword from "../components/SetUserPassword";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
name: string,
|
name: string,
|
||||||
@@ -37,7 +42,7 @@ type Props = {
|
|||||||
|
|
||||||
// dispatcher functions
|
// dispatcher functions
|
||||||
deleteUser: (user: User, callback?: () => void) => void,
|
deleteUser: (user: User, callback?: () => void) => void,
|
||||||
fetchUser: (string, string) => void,
|
fetchUserByName: (string, string) => void,
|
||||||
|
|
||||||
// context objects
|
// context objects
|
||||||
t: string => string,
|
t: string => string,
|
||||||
@@ -47,7 +52,7 @@ type Props = {
|
|||||||
|
|
||||||
class SingleUser extends React.Component<Props> {
|
class SingleUser extends React.Component<Props> {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.fetchUser(this.props.usersLink, this.props.name);
|
this.props.fetchUserByName(this.props.usersLink, this.props.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
userDeleted = () => {
|
userDeleted = () => {
|
||||||
@@ -97,6 +102,10 @@ class SingleUser extends React.Component<Props> {
|
|||||||
path={`${url}/edit`}
|
path={`${url}/edit`}
|
||||||
component={() => <EditUser user={user} />}
|
component={() => <EditUser user={user} />}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path={`${url}/password`}
|
||||||
|
component={() => <SetUserPassword user={user} />}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="column">
|
<div className="column">
|
||||||
<Navigation>
|
<Navigation>
|
||||||
@@ -106,6 +115,10 @@ class SingleUser extends React.Component<Props> {
|
|||||||
label={t("single-user.information-label")}
|
label={t("single-user.information-label")}
|
||||||
/>
|
/>
|
||||||
<EditUserNavLink user={user} editUrl={`${url}/edit`} />
|
<EditUserNavLink user={user} editUrl={`${url}/edit`} />
|
||||||
|
<SetPasswordNavLink
|
||||||
|
user={user}
|
||||||
|
passwordUrl={`${url}/password`}
|
||||||
|
/>
|
||||||
</Section>
|
</Section>
|
||||||
<Section label={t("single-user.actions-label")}>
|
<Section label={t("single-user.actions-label")}>
|
||||||
<DeleteUserNavLink user={user} deleteUser={this.deleteUser} />
|
<DeleteUserNavLink user={user} deleteUser={this.deleteUser} />
|
||||||
@@ -138,8 +151,8 @@ const mapStateToProps = (state, ownProps) => {
|
|||||||
|
|
||||||
const mapDispatchToProps = dispatch => {
|
const mapDispatchToProps = dispatch => {
|
||||||
return {
|
return {
|
||||||
fetchUser: (link: string, name: string) => {
|
fetchUserByName: (link: string, name: string) => {
|
||||||
dispatch(fetchUser(link, name));
|
dispatch(fetchUserByName(link, name));
|
||||||
},
|
},
|
||||||
deleteUser: (user: User, callback?: () => void) => {
|
deleteUser: (user: User, callback?: () => void) => {
|
||||||
dispatch(deleteUser(user, callback));
|
dispatch(deleteUser(user, callback));
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export const MODIFY_USER = "scm/users/MODIFY_USER";
|
|||||||
export const MODIFY_USER_PENDING = `${MODIFY_USER}_${types.PENDING_SUFFIX}`;
|
export const MODIFY_USER_PENDING = `${MODIFY_USER}_${types.PENDING_SUFFIX}`;
|
||||||
export const MODIFY_USER_SUCCESS = `${MODIFY_USER}_${types.SUCCESS_SUFFIX}`;
|
export const MODIFY_USER_SUCCESS = `${MODIFY_USER}_${types.SUCCESS_SUFFIX}`;
|
||||||
export const MODIFY_USER_FAILURE = `${MODIFY_USER}_${types.FAILURE_SUFFIX}`;
|
export const MODIFY_USER_FAILURE = `${MODIFY_USER}_${types.FAILURE_SUFFIX}`;
|
||||||
|
export const MODIFY_USER_RESET = `${MODIFY_USER}_${types.RESET_SUFFIX}`;
|
||||||
|
|
||||||
export const DELETE_USER = "scm/users/DELETE";
|
export const DELETE_USER = "scm/users/DELETE";
|
||||||
export const DELETE_USER_PENDING = `${DELETE_USER}_${types.PENDING_SUFFIX}`;
|
export const DELETE_USER_PENDING = `${DELETE_USER}_${types.PENDING_SUFFIX}`;
|
||||||
@@ -87,12 +88,20 @@ export function fetchUsersFailure(url: string, error: Error): Action {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//fetch user
|
//fetch user
|
||||||
export function fetchUser(link: string, name: string) {
|
export function fetchUserByName(link: string, name: string) {
|
||||||
const userUrl = link.endsWith("/") ? link + name : link + "/" + name;
|
const userUrl = link.endsWith("/") ? link + name : link + "/" + name;
|
||||||
|
return fetchUser(userUrl, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchUserByLink(user: User) {
|
||||||
|
return fetchUser(user._links.self.href, user.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchUser(link: string, name: string) {
|
||||||
return function(dispatch: any) {
|
return function(dispatch: any) {
|
||||||
dispatch(fetchUserPending(name));
|
dispatch(fetchUserPending(name));
|
||||||
return apiClient
|
return apiClient
|
||||||
.get(userUrl)
|
.get(link)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
return response.json();
|
return response.json();
|
||||||
})
|
})
|
||||||
@@ -195,6 +204,9 @@ export function modifyUser(user: User, callback?: () => void) {
|
|||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.then(() => {
|
||||||
|
dispatch(fetchUserByLink(user));
|
||||||
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
dispatch(modifyUserFailure(user, err));
|
dispatch(modifyUserFailure(user, err));
|
||||||
});
|
});
|
||||||
@@ -228,6 +240,13 @@ export function modifyUserFailure(user: User, error: Error): Action {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function modifyUserReset(user: User): Action {
|
||||||
|
return {
|
||||||
|
type: MODIFY_USER_RESET,
|
||||||
|
itemId: user.name
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
//delete user
|
//delete user
|
||||||
|
|
||||||
export function deleteUser(user: User, callback?: () => void) {
|
export function deleteUser(user: User, callback?: () => void) {
|
||||||
@@ -365,9 +384,6 @@ function byNamesReducer(state: any = {}, action: any = {}) {
|
|||||||
case FETCH_USER_SUCCESS:
|
case FETCH_USER_SUCCESS:
|
||||||
return reducerByName(state, action.payload.name, action.payload);
|
return reducerByName(state, action.payload.name, action.payload);
|
||||||
|
|
||||||
case MODIFY_USER_SUCCESS:
|
|
||||||
return reducerByName(state, action.payload.name, action.payload);
|
|
||||||
|
|
||||||
case DELETE_USER_SUCCESS:
|
case DELETE_USER_SUCCESS:
|
||||||
const newUserByNames = deleteUserInUsersByNames(
|
const newUserByNames = deleteUserInUsersByNames(
|
||||||
state,
|
state,
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ import reducer, {
|
|||||||
FETCH_USERS_FAILURE,
|
FETCH_USERS_FAILURE,
|
||||||
FETCH_USERS_PENDING,
|
FETCH_USERS_PENDING,
|
||||||
FETCH_USERS_SUCCESS,
|
FETCH_USERS_SUCCESS,
|
||||||
fetchUser,
|
fetchUserByLink,
|
||||||
|
fetchUserByName,
|
||||||
fetchUserSuccess,
|
fetchUserSuccess,
|
||||||
getFetchUserFailure,
|
getFetchUserFailure,
|
||||||
fetchUsers,
|
fetchUsers,
|
||||||
@@ -33,7 +34,6 @@ import reducer, {
|
|||||||
MODIFY_USER_PENDING,
|
MODIFY_USER_PENDING,
|
||||||
MODIFY_USER_SUCCESS,
|
MODIFY_USER_SUCCESS,
|
||||||
modifyUser,
|
modifyUser,
|
||||||
modifyUserSuccess,
|
|
||||||
getUsersFromState,
|
getUsersFromState,
|
||||||
FETCH_USERS,
|
FETCH_USERS,
|
||||||
getFetchUsersFailure,
|
getFetchUsersFailure,
|
||||||
@@ -124,6 +124,7 @@ const response = {
|
|||||||
|
|
||||||
const URL = "users";
|
const URL = "users";
|
||||||
const USERS_URL = "/api/v2/users";
|
const USERS_URL = "/api/v2/users";
|
||||||
|
const USER_ZAPHOD_URL = "http://localhost:8081/api/v2/users/zaphod";
|
||||||
|
|
||||||
const error = new Error("KAPUTT");
|
const error = new Error("KAPUTT");
|
||||||
|
|
||||||
@@ -166,11 +167,37 @@ describe("users fetch()", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should sucessfully fetch single user", () => {
|
it("should sucessfully fetch single user by name", () => {
|
||||||
fetchMock.getOnce(USERS_URL + "/zaphod", userZaphod);
|
fetchMock.getOnce(USERS_URL + "/zaphod", userZaphod);
|
||||||
|
|
||||||
const store = mockStore({});
|
const store = mockStore({});
|
||||||
return store.dispatch(fetchUser(URL, "zaphod")).then(() => {
|
return store.dispatch(fetchUserByName(URL, "zaphod")).then(() => {
|
||||||
|
const actions = store.getActions();
|
||||||
|
expect(actions[0].type).toEqual(FETCH_USER_PENDING);
|
||||||
|
expect(actions[1].type).toEqual(FETCH_USER_SUCCESS);
|
||||||
|
expect(actions[1].payload).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail fetching single user by name on HTTP 500", () => {
|
||||||
|
fetchMock.getOnce(USERS_URL + "/zaphod", {
|
||||||
|
status: 500
|
||||||
|
});
|
||||||
|
|
||||||
|
const store = mockStore({});
|
||||||
|
return store.dispatch(fetchUserByName(URL, "zaphod")).then(() => {
|
||||||
|
const actions = store.getActions();
|
||||||
|
expect(actions[0].type).toEqual(FETCH_USER_PENDING);
|
||||||
|
expect(actions[1].type).toEqual(FETCH_USER_FAILURE);
|
||||||
|
expect(actions[1].payload).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should sucessfully fetch single user", () => {
|
||||||
|
fetchMock.getOnce(USER_ZAPHOD_URL, userZaphod);
|
||||||
|
|
||||||
|
const store = mockStore({});
|
||||||
|
return store.dispatch(fetchUserByLink(userZaphod)).then(() => {
|
||||||
const actions = store.getActions();
|
const actions = store.getActions();
|
||||||
expect(actions[0].type).toEqual(FETCH_USER_PENDING);
|
expect(actions[0].type).toEqual(FETCH_USER_PENDING);
|
||||||
expect(actions[1].type).toEqual(FETCH_USER_SUCCESS);
|
expect(actions[1].type).toEqual(FETCH_USER_SUCCESS);
|
||||||
@@ -179,12 +206,12 @@ describe("users fetch()", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should fail fetching single user on HTTP 500", () => {
|
it("should fail fetching single user on HTTP 500", () => {
|
||||||
fetchMock.getOnce(USERS_URL + "/zaphod", {
|
fetchMock.getOnce(USER_ZAPHOD_URL, {
|
||||||
status: 500
|
status: 500
|
||||||
});
|
});
|
||||||
|
|
||||||
const store = mockStore({});
|
const store = mockStore({});
|
||||||
return store.dispatch(fetchUser(URL, "zaphod")).then(() => {
|
return store.dispatch(fetchUserByLink(userZaphod)).then(() => {
|
||||||
const actions = store.getActions();
|
const actions = store.getActions();
|
||||||
expect(actions[0].type).toEqual(FETCH_USER_PENDING);
|
expect(actions[0].type).toEqual(FETCH_USER_PENDING);
|
||||||
expect(actions[1].type).toEqual(FETCH_USER_FAILURE);
|
expect(actions[1].type).toEqual(FETCH_USER_FAILURE);
|
||||||
@@ -242,23 +269,26 @@ describe("users fetch()", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("successfully update user", () => {
|
it("successfully update user", () => {
|
||||||
fetchMock.putOnce("http://localhost:8081/api/v2/users/zaphod", {
|
fetchMock.putOnce(USER_ZAPHOD_URL, {
|
||||||
status: 204
|
status: 204
|
||||||
});
|
});
|
||||||
|
fetchMock.getOnce(USER_ZAPHOD_URL, userZaphod);
|
||||||
|
|
||||||
const store = mockStore({});
|
const store = mockStore({});
|
||||||
return store.dispatch(modifyUser(userZaphod)).then(() => {
|
return store.dispatch(modifyUser(userZaphod)).then(() => {
|
||||||
const actions = store.getActions();
|
const actions = store.getActions();
|
||||||
expect(actions.length).toBe(2);
|
expect(actions.length).toBe(3);
|
||||||
expect(actions[0].type).toEqual(MODIFY_USER_PENDING);
|
expect(actions[0].type).toEqual(MODIFY_USER_PENDING);
|
||||||
expect(actions[1].type).toEqual(MODIFY_USER_SUCCESS);
|
expect(actions[1].type).toEqual(MODIFY_USER_SUCCESS);
|
||||||
|
expect(actions[2].type).toEqual(FETCH_USER_PENDING);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should call callback, after successful modified user", () => {
|
it("should call callback, after successful modified user", () => {
|
||||||
fetchMock.putOnce("http://localhost:8081/api/v2/users/zaphod", {
|
fetchMock.putOnce(USER_ZAPHOD_URL, {
|
||||||
status: 204
|
status: 204
|
||||||
});
|
});
|
||||||
|
fetchMock.getOnce(USER_ZAPHOD_URL, userZaphod);
|
||||||
|
|
||||||
let called = false;
|
let called = false;
|
||||||
const callMe = () => {
|
const callMe = () => {
|
||||||
@@ -415,20 +445,6 @@ describe("users reducer", () => {
|
|||||||
expect(newState.byNames["ford"]).toBe(userFord);
|
expect(newState.byNames["ford"]).toBe(userFord);
|
||||||
expect(newState.list.entries).toEqual(["zaphod"]);
|
expect(newState.list.entries).toEqual(["zaphod"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should update state according to MODIFY_USER_SUCCESS action", () => {
|
|
||||||
const newState = reducer(
|
|
||||||
{
|
|
||||||
byNames: {
|
|
||||||
ford: {
|
|
||||||
name: "ford"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
modifyUserSuccess(userFord)
|
|
||||||
);
|
|
||||||
expect(newState.byNames["ford"]).toBe(userFord);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("selector tests", () => {
|
describe("selector tests", () => {
|
||||||
|
|||||||
1341
scm-ui/yarn.lock
1341
scm-ui/yarn.lock
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@ import com.webcohesion.enunciate.metadata.rs.StatusCodes;
|
|||||||
import com.webcohesion.enunciate.metadata.rs.TypeHint;
|
import com.webcohesion.enunciate.metadata.rs.TypeHint;
|
||||||
import sonia.scm.NotFoundException;
|
import sonia.scm.NotFoundException;
|
||||||
import sonia.scm.PageResult;
|
import sonia.scm.PageResult;
|
||||||
|
import sonia.scm.repository.Branch;
|
||||||
import sonia.scm.repository.Branches;
|
import sonia.scm.repository.Branches;
|
||||||
import sonia.scm.repository.Changeset;
|
import sonia.scm.repository.Changeset;
|
||||||
import sonia.scm.repository.ChangesetPagingResult;
|
import sonia.scm.repository.ChangesetPagingResult;
|
||||||
@@ -26,6 +27,7 @@ import javax.ws.rs.Produces;
|
|||||||
import javax.ws.rs.QueryParam;
|
import javax.ws.rs.QueryParam;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class BranchRootResource {
|
public class BranchRootResource {
|
||||||
|
|
||||||
@@ -124,6 +126,49 @@ public class BranchRootResource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Path("{branch}/diffchangesets/{otherBranchName}")
|
||||||
|
@GET
|
||||||
|
@StatusCodes({
|
||||||
|
@ResponseCode(code = 200, condition = "success"),
|
||||||
|
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
|
||||||
|
@ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the changeset"),
|
||||||
|
@ResponseCode(code = 404, condition = "not found, no changesets available in the repository"),
|
||||||
|
@ResponseCode(code = 500, condition = "internal server error")
|
||||||
|
})
|
||||||
|
@Produces(VndMediaType.CHANGESET_COLLECTION)
|
||||||
|
@TypeHint(CollectionDto.class)
|
||||||
|
public Response changesetDiff(@PathParam("namespace") String namespace,
|
||||||
|
@PathParam("name") String name,
|
||||||
|
@PathParam("branch") String branchName,
|
||||||
|
@PathParam("otherBranchName") String otherBranchName,
|
||||||
|
@DefaultValue("0") @QueryParam("page") int page,
|
||||||
|
@DefaultValue("10") @QueryParam("pageSize") int pageSize) throws Exception {
|
||||||
|
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
|
||||||
|
List<Branch> allBranches = repositoryService.getBranchesCommand().getBranches().getBranches();
|
||||||
|
if (allBranches.stream().noneMatch(branch -> branchName.equals(branch.getName()))) {
|
||||||
|
throw new NotFoundException("branch", branchName);
|
||||||
|
}
|
||||||
|
if (allBranches.stream().noneMatch(branch -> otherBranchName.equals(branch.getName()))) {
|
||||||
|
throw new NotFoundException("branch", otherBranchName);
|
||||||
|
}
|
||||||
|
Repository repository = repositoryService.getRepository();
|
||||||
|
RepositoryPermissions.read(repository).check();
|
||||||
|
ChangesetPagingResult changesets = new PagedLogCommandBuilder(repositoryService)
|
||||||
|
.page(page)
|
||||||
|
.pageSize(pageSize)
|
||||||
|
.create()
|
||||||
|
.setBranch(branchName)
|
||||||
|
.setAncestorChangeset(otherBranchName)
|
||||||
|
.getChangesets();
|
||||||
|
if (changesets != null && changesets.getChangesets() != null) {
|
||||||
|
PageResult<Changeset> pageResult = new PageResult<>(changesets.getChangesets(), changesets.getTotal());
|
||||||
|
return Response.ok(branchChangesetCollectionToDtoMapper.map(page, pageSize, pageResult, repository, branchName)).build();
|
||||||
|
} else {
|
||||||
|
return Response.ok().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the branches for a repository.
|
* Returns the branches for a repository.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ public abstract class BranchToBranchDtoMapper {
|
|||||||
Links.Builder linksBuilder = linkingTo()
|
Links.Builder linksBuilder = linkingTo()
|
||||||
.self(resourceLinks.branch().self(namespaceAndName, target.getName()))
|
.self(resourceLinks.branch().self(namespaceAndName, target.getName()))
|
||||||
.single(linkBuilder("history", resourceLinks.branch().history(namespaceAndName, target.getName())).build())
|
.single(linkBuilder("history", resourceLinks.branch().history(namespaceAndName, target.getName())).build())
|
||||||
|
.single(linkBuilder("changesetDiff", resourceLinks.branch().changesetDiff(namespaceAndName, target.getName())).build())
|
||||||
.single(linkBuilder("changeset", resourceLinks.changeset().changeset(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getRevision())).build())
|
.single(linkBuilder("changeset", resourceLinks.changeset().changeset(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getRevision())).build())
|
||||||
.single(linkBuilder("source", resourceLinks.source().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getRevision())).build());
|
.single(linkBuilder("source", resourceLinks.source().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getRevision())).build());
|
||||||
target.add(linksBuilder.build());
|
target.add(linksBuilder.build());
|
||||||
|
|||||||
@@ -322,6 +322,10 @@ class ResourceLinks {
|
|||||||
public String history(NamespaceAndName namespaceAndName, String branch) {
|
public String history(NamespaceAndName namespaceAndName, String branch) {
|
||||||
return branchLinkBuilder.method("getRepositoryResource").parameters(namespaceAndName.getNamespace(), namespaceAndName.getName()).method("branches").parameters().method("history").parameters(branch).href();
|
return branchLinkBuilder.method("getRepositoryResource").parameters(namespaceAndName.getNamespace(), namespaceAndName.getName()).method("branches").parameters().method("history").parameters(branch).href();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String changesetDiff(NamespaceAndName namespaceAndName, String branch) {
|
||||||
|
return branchLinkBuilder.method("getRepositoryResource").parameters(namespaceAndName.getNamespace(), namespaceAndName.getName()).method("branches").parameters().method("changesetDiff").parameters(branch, "").href() + "{otherBranch}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public BranchCollectionLinks branchCollection() {
|
public BranchCollectionLinks branchCollection() {
|
||||||
|
|||||||
Reference in New Issue
Block a user